From 30185f3acd6c7367d82943d0ab57937870ca7235 Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 6 Apr 2025 22:17:52 +0200 Subject: [PATCH 01/49] GH-602: big bunch of changes --- masq/src/commands/scan_command.rs | 12 +- masq_lib/src/messages.rs | 42 +- node/src/accountant/mod.rs | 852 +++++++------- .../payable_scanner/mod.rs | 8 +- node/src/accountant/scanners/mod.rs | 1003 ++++++++++++++--- node/src/accountant/test_utils.rs | 230 +--- node/src/blockchain/blockchain_bridge.rs | 3 +- node/src/daemon/setup_reporter.rs | 6 +- .../src/db_config/persistent_configuration.rs | 3 +- node/src/node_configurator/configurator.rs | 7 - .../unprivileged_parse_args_configuration.rs | 4 - node/src/sub_lib/accountant.rs | 3 - node/src/sub_lib/combined_parameters.rs | 10 +- node/src/sub_lib/utils.rs | 1 + node/src/test_utils/mod.rs | 2 +- node/src/test_utils/recorder.rs | 5 +- .../test_utils/recorder_stop_conditions.rs | 2 +- 17 files changed, 1285 insertions(+), 908 deletions(-) diff --git a/masq/src/commands/scan_command.rs b/masq/src/commands/scan_command.rs index 82359d2a5..6faa7be1e 100644 --- a/masq/src/commands/scan_command.rs +++ b/masq/src/commands/scan_command.rs @@ -3,7 +3,7 @@ use crate::command_context::CommandContext; use crate::commands::commands_common::{transaction, Command, CommandError}; use clap::{App, Arg, SubCommand}; -use masq_lib::messages::{ScanType, UiScanRequest, UiScanResponse}; +use masq_lib::messages::{CommendableScanType, UiScanRequest, UiScanResponse}; use std::fmt::Debug; use std::str::FromStr; @@ -34,7 +34,7 @@ pub fn scan_subcommand() -> App<'static, 'static> { impl Command for ScanCommand { fn execute(&self, context: &mut dyn CommandContext) -> Result<(), CommandError> { let input = UiScanRequest { - scan_type: match ScanType::from_str(&self.name) { + scan_type: match CommendableScanType::from_str(&self.name) { Ok(st) => st, Err(s) => panic!("clap schema does not restrict scan type properly: {}", s), }, @@ -102,12 +102,12 @@ mod tests { #[test] fn scan_command_works() { - scan_command_for_name("payables", ScanType::Payables); - scan_command_for_name("receivables", ScanType::Receivables); - scan_command_for_name("pendingpayables", ScanType::PendingPayables); + scan_command_for_name("payables", CommendableScanType::Payables); + scan_command_for_name("receivables", CommendableScanType::Receivables); + scan_command_for_name("pendingpayables", CommendableScanType::PendingPayables); } - fn scan_command_for_name(name: &str, scan_type: ScanType) { + fn scan_command_for_name(name: &str, scan_type: CommendableScanType) { let transact_params_arc = Arc::new(Mutex::new(vec![])); let mut context = CommandContextMock::new() .transact_params(&transact_params_arc) diff --git a/masq_lib/src/messages.rs b/masq_lib/src/messages.rs index 45842e419..973b9037b 100644 --- a/masq_lib/src/messages.rs +++ b/masq_lib/src/messages.rs @@ -525,8 +525,6 @@ pub struct UiRatePack { #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct UiScanIntervals { - #[serde(rename = "pendingPayableSec")] - pub pending_payable_sec: u64, #[serde(rename = "payableSec")] pub payable_sec: u64, #[serde(rename = "receivableSec")] @@ -779,20 +777,18 @@ pub struct UiRecoverWalletsResponse {} conversation_message!(UiRecoverWalletsResponse, "recoverWallets"); #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash)] -pub enum ScanType { +pub enum CommendableScanType { Payables, Receivables, - PendingPayables, } -impl FromStr for ScanType { +impl FromStr for CommendableScanType { type Err = String; fn from_str(s: &str) -> Result { match s { - s if &s.to_lowercase() == "payables" => Ok(ScanType::Payables), - s if &s.to_lowercase() == "receivables" => Ok(ScanType::Receivables), - s if &s.to_lowercase() == "pendingpayables" => Ok(ScanType::PendingPayables), + s if &s.to_lowercase() == "payables" => Ok(CommendableScanType::Payables), + s if &s.to_lowercase() == "receivables" => Ok(CommendableScanType::Receivables), s => Err(format!("Unrecognized ScanType: '{}'", s)), } } @@ -801,7 +797,7 @@ impl FromStr for ScanType { #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct UiScanRequest { #[serde(rename = "scanType")] - pub scan_type: ScanType, + pub scan_type: CommendableScanType, } conversation_message!(UiScanRequest, "scan"); @@ -1157,34 +1153,26 @@ mod tests { #[test] fn scan_type_from_string_happy_path() { - let result: Vec = vec![ - "Payables", - "pAYABLES", - "Receivables", - "rECEIVABLES", - "PendingPayables", - "pENDINGpAYABLES", - ] - .into_iter() - .map(|s| ScanType::from_str(s).unwrap()) - .collect(); + let result: Vec = + vec!["Payables", "pAYABLES", "Receivables", "rECEIVABLES"] + .into_iter() + .map(|s| CommendableScanType::from_str(s).unwrap()) + .collect(); assert_eq!( result, vec![ - ScanType::Payables, - ScanType::Payables, - ScanType::Receivables, - ScanType::Receivables, - ScanType::PendingPayables, - ScanType::PendingPayables, + CommendableScanType::Payables, + CommendableScanType::Payables, + CommendableScanType::Receivables, + CommendableScanType::Receivables, ] ) } #[test] fn scan_type_from_string_error() { - let result = ScanType::from_str("unrecognized"); + let result = CommendableScanType::from_str("unrecognized"); assert_eq!( result, diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 0a74d076c..1f56dfba8 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -25,7 +25,7 @@ use crate::accountant::financials::visibility_restricted_module::{ use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; -use crate::accountant::scanners::{BeginScanError, ScanSchedulers, Scanners}; +use crate::accountant::scanners::{BeginScanError, ScanSchedulers, ScanType, Scanners, GuardedStartableScanner}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, PendingPayableFingerprintSeeds, RetrieveTransactions}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; @@ -57,10 +57,10 @@ use itertools::Either; use itertools::Itertools; use masq_lib::crash_point::CrashPoint; use masq_lib::logger::Logger; -use masq_lib::messages::UiFinancialsResponse; +use masq_lib::messages::{CommendableScanType, UiFinancialsResponse}; use masq_lib::messages::{FromMessageBody, ToMessageBody, UiFinancialsRequest}; use masq_lib::messages::{ - QueryResults, ScanType, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, + QueryResults, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, UiScanRequest, }; use masq_lib::ui_gateway::MessageTarget::ClientId; @@ -95,7 +95,7 @@ pub struct Accountant { outbound_payments_instructions_sub_opt: Option>, qualified_payables_sub_opt: Option>, retrieve_transactions_sub_opt: Option>, - request_transaction_receipts_subs_opt: Option>, + request_transaction_receipts_sub_opt: Option>, report_inbound_payments_sub_opt: Option>, report_sent_payables_sub_opt: Option>, ui_message_sub_opt: Option>, @@ -141,17 +141,17 @@ pub struct SentPayables { } #[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] -pub struct ScanForPayables { +pub struct ScanForPendingPayables { pub response_skeleton_opt: Option, } #[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] -pub struct ScanForReceivables { +pub struct ScanForPayables { pub response_skeleton_opt: Option, } #[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] -pub struct ScanForPendingPayables { +pub struct ScanForReceivables { pub response_skeleton_opt: Option, } @@ -194,9 +194,6 @@ impl Handler for Accountant { "Started with --scans on; starting database and blockchain scans" ); - ctx.notify(ScanForPendingPayables { - response_skeleton_opt: None, - }); ctx.notify(ScanForPayables { response_skeleton_opt: None, }); @@ -247,21 +244,21 @@ impl Handler for Accountant { } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ScanForPayables, ctx: &mut Self::Context) -> Self::Result { - self.handle_request_of_scan_for_payable(msg.response_skeleton_opt); - self.schedule_next_scan(ScanType::Payables, ctx); + fn handle(&mut self, msg: ScanForPendingPayables, _ctx: &mut Self::Context) -> Self::Result { + let response_skeleton = msg.response_skeleton_opt; + self.handle_request_of_scan_for_pending_payable(response_skeleton); } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { - self.handle_request_of_scan_for_pending_payable(msg.response_skeleton_opt); - self.schedule_next_scan(ScanType::PendingPayables, ctx); + fn handle(&mut self, msg: ScanForPayables, _ctx: &mut Self::Context) -> Self::Result { + let response_skeleton = msg.response_skeleton_opt; + self.handle_request_of_scan_for_payable(response_skeleton); } } @@ -284,6 +281,7 @@ impl Handler for Accountant { self.scanners.payable.mark_as_ended(&self.logger); } ScanType::PendingPayables => { + // TODO check if this is still tested self.scanners.pending_payable.mark_as_ended(&self.logger); } ScanType::Receivables => { @@ -377,13 +375,11 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ReportTransactionReceipts, _ctx: &mut Self::Context) -> Self::Result { - if let Some(node_to_ui_msg) = self.scanners.pending_payable.finish_scan(msg, &self.logger) { - self.ui_message_sub_opt - .as_ref() - .expect("UIGateway is not bound") - .try_send(node_to_ui_msg) - .expect("UIGateway is dead"); - } + let response_skeleton_opt = msg.response_skeleton_opt; + + let _ = self.scanners.pending_payable.finish_scan(msg, &self.logger); + + self.handle_request_of_scan_for_payable(response_skeleton_opt) } } @@ -452,7 +448,7 @@ impl Accountant { report_sent_payables_sub_opt: None, retrieve_transactions_sub_opt: None, report_inbound_payments_sub_opt: None, - request_transaction_receipts_subs_opt: None, + request_transaction_receipts_sub_opt: None, ui_message_sub_opt: None, message_id_generator: Box::new(MessageIdGeneratorReal::default()), logger: Logger::new("Accountant"), @@ -571,7 +567,7 @@ impl Accountant { Some(msg.peer_actors.blockchain_bridge.qualified_payables); self.report_sent_payables_sub_opt = Some(msg.peer_actors.accountant.report_sent_payments); self.ui_message_sub_opt = Some(msg.peer_actors.ui_gateway.node_to_ui_message_sub); - self.request_transaction_receipts_subs_opt = Some( + self.request_transaction_receipts_sub_opt = Some( msg.peer_actors .blockchain_bridge .request_transaction_receipts, @@ -843,15 +839,16 @@ impl Accountant { &mut self, response_skeleton_opt: Option, ) { - let result = match self.consuming_wallet_opt.clone() { - Some(consuming_wallet) => self.scanners.payable.begin_scan( - consuming_wallet, - SystemTime::now(), - response_skeleton_opt, - &self.logger, - ), - None => Err(BeginScanError::NoConsumingWalletFound), - }; + let result: Result = + match self.consuming_wallet_opt.as_ref() { + Some(consuming_wallet) => self.scanners.start_scan_guarded( + consuming_wallet, + SystemTime::now(), + response_skeleton_opt, + &self.logger, + ), + None => Err(BeginScanError::NoConsumingWalletFound), + }; match result { Ok(scan_message) => { @@ -873,28 +870,37 @@ impl Accountant { &mut self, response_skeleton_opt: Option, ) { - let result = match self.consuming_wallet_opt.clone() { - Some(consuming_wallet) => self.scanners.pending_payable.begin_scan( - consuming_wallet, // This argument is not used and is therefore irrelevant - SystemTime::now(), - response_skeleton_opt, - &self.logger, - ), - None => Err(BeginScanError::NoConsumingWalletFound), - }; + // let result: Result = self.scanners.start_scan_guarded(); + + let result: Result = + match self.consuming_wallet_opt.as_ref() { + Some(consuming_wallet) => self.scanners.start_scan_guarded( + consuming_wallet, // This argument is not used and is therefore irrelevant + SystemTime::now(), + response_skeleton_opt, + &self.logger, + ), + None => Err(BeginScanError::NoConsumingWalletFound), + }; match result { Ok(scan_message) => self - .request_transaction_receipts_subs_opt + .request_transaction_receipts_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"), - Err(e) => e.handle_error( - &self.logger, - ScanType::PendingPayables, - response_skeleton_opt.is_some(), - ), + Err(e) => { + if e == BeginScanError::NothingToProcess { + todo!() + } + + e.handle_error( + &self.logger, + ScanType::PendingPayables, + response_skeleton_opt.is_some(), + ) + } } } @@ -902,12 +908,15 @@ impl Accountant { &mut self, response_skeleton_opt: Option, ) { - match self.scanners.receivable.begin_scan( - self.earning_wallet.clone(), - SystemTime::now(), - response_skeleton_opt, - &self.logger, - ) { + let result: Result = + self.scanners.start_scan_guarded( + &self.earning_wallet, + SystemTime::now(), + response_skeleton_opt, + &self.logger, + ); + + match result { Ok(scan_message) => self .retrieve_transactions_sub_opt .as_ref() @@ -925,15 +934,14 @@ impl Accountant { fn handle_externally_triggered_scan( &mut self, _ctx: &mut Context, - scan_type: ScanType, + scan_type: CommendableScanType, response_skeleton: ResponseSkeleton, ) { match scan_type { - ScanType::Payables => self.handle_request_of_scan_for_payable(Some(response_skeleton)), - ScanType::PendingPayables => { - self.handle_request_of_scan_for_pending_payable(Some(response_skeleton)); + CommendableScanType::Payables => { + self.handle_request_of_scan_for_pending_payable(Some(response_skeleton)) } - ScanType::Receivables => { + CommendableScanType::Receivables => { self.handle_request_of_scan_for_receivable(Some(response_skeleton)) } } @@ -1047,17 +1055,11 @@ mod tests { use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; use crate::accountant::scanners::test_utils::protect_payables_in_test; - use crate::accountant::scanners::BeginScanError; + use crate::accountant::scanners::{BeginScanError, PeriodicalScanScheduler}; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; - use crate::accountant::test_utils::{ - bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_payables, - BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, NullScanner, - PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, - PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, - ReceivableDaoMock, ScannerMock, - }; + use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_payables, make_pending_payable_fingerprint, BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock}; use crate::accountant::test_utils::{AccountantBuilder, BannedDaoMock}; use crate::accountant::Accountant; use crate::blockchain::blockchain_bridge::BlockchainBridge; @@ -1083,7 +1085,7 @@ mod tests { use crate::test_utils::recorder::Recorder; use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; - use crate::test_utils::unshared_test_utils::notify_handlers::NotifyLaterHandleMock; + use crate::test_utils::unshared_test_utils::notify_handlers::{NotifyHandleMock, NotifyLaterHandleMock}; use crate::test_utils::unshared_test_utils::system_killer_actor::SystemKillerActor; use crate::test_utils::unshared_test_utils::{ assert_on_initialization_with_panic_on_migration, make_bc_with_defaults, @@ -1100,7 +1102,7 @@ mod tests { }; use masq_lib::messages::TopRecordsOrdering::{Age, Balance}; use masq_lib::messages::{ - CustomQueries, RangeQuery, ScanType, TopRecordsConfig, UiFinancialStatistics, + CustomQueries, RangeQuery, TopRecordsConfig, UiFinancialStatistics, UiMessageError, UiPayableAccount, UiReceivableAccount, UiScanRequest, UiScanResponse, }; use masq_lib::test_utils::logging::init_test_logging; @@ -1117,6 +1119,7 @@ mod tests { use std::sync::Mutex; use std::time::Duration; use std::vec; + use crate::accountant::scanners::local_test_utils::{NullScanner, ScannerMock}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; impl Handler> for Accountant { @@ -1230,27 +1233,14 @@ mod tests { ); let financial_statistics = result.financial_statistics().clone(); - let assert_scan_scheduler = |scan_type: ScanType, expected_scan_interval: Duration| { - assert_eq!( - result - .scan_schedulers - .schedulers - .get(&scan_type) - .unwrap() - .interval(), - expected_scan_interval - ) - }; let default_scan_intervals = ScanIntervals::default(); - assert_scan_scheduler( + assert_scan_scheduler::( + &result, ScanType::Payables, default_scan_intervals.payable_scan_interval, ); - assert_scan_scheduler( - ScanType::PendingPayables, - default_scan_intervals.pending_payable_scan_interval, - ); - assert_scan_scheduler( + assert_scan_scheduler::( + &result, ScanType::Receivables, default_scan_intervals.receivable_scan_interval, ); @@ -1267,6 +1257,25 @@ mod tests { assert_eq!(financial_statistics.total_paid_payable_wei, 0); } + fn assert_scan_scheduler( + accountant: &Accountant, + scan_type: ScanType, + expected_scan_interval: Duration, + ) { + assert_eq!( + accountant + .scan_schedulers + .schedulers + .get(&scan_type) + .unwrap() + .as_any() + .downcast_ref::>() + .unwrap() + .interval, + expected_scan_interval + ) + } + #[test] fn accountant_handles_config_change_msg() { assert_handling_of_config_change_msg( @@ -1335,7 +1344,6 @@ mod tests { config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), }); let receivable_dao = ReceivableDaoMock::new() .new_delinquencies_result(vec![]) @@ -1354,7 +1362,7 @@ mod tests { let ui_message = NodeFromUiMessage { client_id: 1234, body: UiScanRequest { - scan_type: ScanType::Receivables, + scan_type: CommendableScanType::Receivables, } .tmb(4321), }; @@ -1382,7 +1390,6 @@ mod tests { config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), }); config.suppress_initial_scans = true; let subject = AccountantBuilder::default() @@ -1424,52 +1431,50 @@ mod tests { #[test] fn scan_payables_request() { - let config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - let consuming_wallet = make_paying_wallet(b"consuming"); - let payable_account = PayableAccount { - wallet: make_wallet("wallet"), - balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), - last_paid_timestamp: SystemTime::now().sub(Duration::from_secs( - (DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 1) as u64, - )), - pending_payable_opt: None, + let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); + config.suppress_initial_scans = true; + let fingerprint = PendingPayableFingerprint { + rowid: 1234, + timestamp: SystemTime::now(), + hash: Default::default(), + attempt: 1, + amount: 1_000_000, + process_error: None, }; - let payable_dao = - PayableDaoMock::new().non_pending_payables_result(vec![payable_account.clone()]); - let subject = AccountantBuilder::default() + let pending_payable_dao = PendingPayableDaoMock::default() + .return_all_errorless_fingerprints_result(vec![fingerprint.clone()]); + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_paying_wallet(b"consuming")) .bootstrapper_config(config) - .consuming_wallet(consuming_wallet.clone()) - .payable_daos(vec![ForPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge = blockchain_bridge + .system_stop_conditions(match_every_type_id!(RequestTransactionReceipts)); + let blockchain_bridge_addr = blockchain_bridge.start(); + subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); let subject_addr = subject.start(); let system = System::new("test"); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); let ui_message = NodeFromUiMessage { client_id: 1234, body: UiScanRequest { - scan_type: ScanType::Payables, + scan_type: CommendableScanType::Payables, } .tmb(4321), }; subject_addr.try_send(ui_message).unwrap(); - System::current().stop(); system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); assert_eq!( - blockchain_bridge_recording.get_record::(0), - &QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(vec![payable_account]), - consuming_wallet, + blockchain_bridge_recording.get_record::(0), + &RequestTransactionReceipts { + pending_payable: vec![fingerprint], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321, - }) + }), } ); } @@ -1727,72 +1732,11 @@ mod tests { } #[test] - fn scan_pending_payables_request() { - let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - config.suppress_initial_scans = true; - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }); - let fingerprint = PendingPayableFingerprint { - rowid: 1234, - timestamp: SystemTime::now(), - hash: Default::default(), - attempt: 1, - amount: 1_000_000, - process_error: None, - }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![fingerprint.clone()]); - let subject = AccountantBuilder::default() - .consuming_wallet(make_paying_wallet(b"consuming")) - .bootstrapper_config(config) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) - .build(); - let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let subject_addr = subject.start(); - let system = System::new("test"); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - let ui_message = NodeFromUiMessage { - client_id: 1234, - body: UiScanRequest { - scan_type: ScanType::PendingPayables, - } - .tmb(4321), - }; - - subject_addr.try_send(ui_message).unwrap(); - - System::current().stop(); - system.run(); - let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!( - blockchain_bridge_recording.get_record::(0), - &RequestTransactionReceipts { - pending_payable: vec![fingerprint], - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }), - } - ); - } - - #[test] - fn scan_request_from_ui_is_handled_in_case_the_scan_is_already_running() { + fn scan_request_from_ui_is_not_handled_in_case_the_scan_is_already_running() { init_test_logging(); - let test_name = "scan_request_from_ui_is_handled_in_case_the_scan_is_already_running"; + let test_name = "scan_request_from_ui_is_not_handled_in_case_the_scan_is_already_running"; let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); config.suppress_initial_scans = true; - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }); let fingerprint = PendingPayableFingerprint { rowid: 1234, timestamp: SystemTime::now(), @@ -1811,18 +1755,18 @@ mod tests { .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let subject_addr = subject.start(); - let system = System::new("test"); + let system = System::new(test_name); + let peer_actors = peer_actors_builder() + .blockchain_bridge(blockchain_bridge) + .build(); let first_message = NodeFromUiMessage { client_id: 1234, body: UiScanRequest { - scan_type: ScanType::PendingPayables, + scan_type: CommendableScanType::Payables, } .tmb(4321), }; let second_message = first_message.clone(); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); subject_addr.try_send(BindMessage { peer_actors }).unwrap(); subject_addr.try_send(first_message).unwrap(); @@ -1838,42 +1782,46 @@ mod tests { assert_eq!(blockchain_bridge_recording.len(), 1); } + // TODO This should probably become a test where the finish_scan method in pending payable scanner + // will throw an error and so the scan for payables is not gonna continue. Maybe include + // a response skeleton #[test] fn report_transaction_receipts_with_response_skeleton_sends_scan_response_to_ui_gateway() { - let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), - }); - let subject = AccountantBuilder::default() - .bootstrapper_config(config) - .build(); - let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); - let subject_addr = subject.start(); - let system = System::new("test"); - let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - let report_transaction_receipts = ReportTransactionReceipts { - fingerprints_with_receipts: vec![], - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }), - }; - - subject_addr.try_send(report_transaction_receipts).unwrap(); - - System::current().stop(); - system.run(); - let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); - assert_eq!( - ui_gateway_recording.get_record::(0), - &NodeToUiMessage { - target: ClientId(1234), - body: UiScanResponse {}.tmb(4321), - } - ); + todo!("don't send the message yet...the payable scanner follows") + // let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); + // config.scan_intervals_opt = Some(ScanIntervals { + // payable_scan_interval: Duration::from_millis(10_000), + // receivable_scan_interval: Duration::from_millis(10_000), + // pending_payable_scan_interval: Duration::from_secs(100), + // }); + // let subject = AccountantBuilder::default() + // .bootstrapper_config(config) + // .build(); + // let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + // let subject_addr = subject.start(); + // let system = System::new("test"); + // let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); + // subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + // let report_transaction_receipts = ReportTransactionReceipts { + // fingerprints_with_receipts: vec![], + // response_skeleton_opt: Some(ResponseSkeleton { + // client_id: 1234, + // context_id: 4321, + // }), + // }; + // + // subject_addr.try_send(report_transaction_receipts).unwrap(); + // + // System::current().stop(); + // system.run(); + // let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + // assert_eq!( + // ui_gateway_recording.get_record::(0), + // &NodeToUiMessage { + // target: ClientId(1234), + // body: UiScanResponse {}.tmb(4321), + // } + // ); } #[test] @@ -2078,18 +2026,16 @@ mod tests { #[test] fn accountant_scans_after_startup() { + // Note: We don't assert on the payable scanner itself because it follows the pending + // payable scanner which is beyond the scope of this test init_test_logging(); let pending_payable_params_arc = Arc::new(Mutex::new(vec![])); - let payable_params_arc = Arc::new(Mutex::new(vec![])); let new_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); let paid_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, _) = make_recorder(); let earning_wallet = make_wallet("earning"); let system = System::new("accountant_scans_after_startup"); let config = bc_from_wallets(make_wallet("buy"), earning_wallet.clone()); - let payable_dao = PayableDaoMock::new() - .non_pending_payables_params(&payable_params_arc) - .non_pending_payables_result(vec![]); let pending_payable_dao = PendingPayableDaoMock::default() .return_all_errorless_fingerprints_params(&pending_payable_params_arc) .return_all_errorless_fingerprints_result(vec![]); @@ -2100,7 +2046,6 @@ mod tests { .paid_delinquencies_result(vec![]); let subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_daos(vec![ForPayableScanner(payable_dao)]) .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) .build(); @@ -2115,13 +2060,11 @@ mod tests { System::current().stop(); system.run(); - let payable_params = payable_params_arc.lock().unwrap(); let pending_payable_params = pending_payable_params_arc.lock().unwrap(); - //proof of calling pieces of scan_for_delinquencies() + // Proof of calling pieces of scan_for_delinquencies() let mut new_delinquencies_params = new_delinquencies_params_arc.lock().unwrap(); let (captured_timestamp, captured_curves) = new_delinquencies_params.remove(0); let paid_delinquencies_params = paid_delinquencies_params_arc.lock().unwrap(); - assert_eq!(*payable_params, vec![()]); assert_eq!(*pending_payable_params, vec![()]); assert!(new_delinquencies_params.is_empty()); assert!( @@ -2132,7 +2075,6 @@ mod tests { assert_eq!(paid_delinquencies_params.len(), 1); assert_eq!(paid_delinquencies_params[0], PaymentThresholds::default()); let tlh = TestLogHandler::new(); - tlh.exists_log_containing("INFO: Accountant: Scanning for payables"); tlh.exists_log_containing("INFO: Accountant: Scanning for pending payable"); tlh.exists_log_containing(&format!( "INFO: Accountant: Scanning for receivables to {}", @@ -2145,14 +2087,16 @@ mod tests { fn periodical_scanning_for_receivables_and_delinquencies_works() { init_test_logging(); let test_name = "periodical_scanning_for_receivables_and_delinquencies_works"; - let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); + let start_scan_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_receivable_params_arc = Arc::new(Mutex::new(vec![])); let system = System::new(test_name); SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions let receivable_scanner = ScannerMock::new() - .begin_scan_params(&begin_scan_params_arc) - .begin_scan_result(Err(BeginScanError::NothingToProcess)) - .begin_scan_result(Ok(RetrieveTransactions { + .started_at_result(None) + .started_at_result(None) + .start_scan_params(&start_scan_params_arc) + .start_scan_result(Err(BeginScanError::NothingToProcess)) + .start_scan_result(Ok(RetrieveTransactions { recipient: make_wallet("some_recipient"), response_skeleton_opt: None, })) @@ -2162,7 +2106,6 @@ mod tests { config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_secs(100), receivable_scan_interval: Duration::from_millis(99), - pending_payable_scan_interval: Duration::from_secs(100), }); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) @@ -2171,7 +2114,7 @@ mod tests { subject.scanners.payable = Box::new(NullScanner::new()); // Skipping subject.scanners.pending_payable = Box::new(NullScanner::new()); // Skipping subject.scanners.receivable = Box::new(receivable_scanner); - subject.scan_schedulers.update_scheduler( + subject.scan_schedulers.update_periodical_scheduler( ScanType::Receivables, Some(Box::new( NotifyLaterHandleMock::default() @@ -2191,34 +2134,19 @@ mod tests { system.run(); let time_after = SystemTime::now(); let notify_later_receivable_params = notify_later_receivable_params_arc.lock().unwrap(); - TestLogHandler::new().exists_log_containing(&format!( + let tlh = TestLogHandler::new(); + tlh.exists_log_containing(&format!( "DEBUG: {test_name}: There was nothing to process during Receivables scan." )); - let mut begin_scan_params = begin_scan_params_arc.lock().unwrap(); - let ( - first_attempt_wallet, - first_attempt_timestamp, - first_attempt_response_skeleton_opt, - first_attempt_logger, - ) = begin_scan_params.remove(0); - let ( - second_attempt_wallet, - second_attempt_timestamp, - second_attempt_response_skeleton_opt, - second_attempt_logger, - ) = begin_scan_params.remove(0); - assert_eq!(first_attempt_wallet, second_attempt_wallet); - assert_eq!(second_attempt_wallet, earning_wallet); - assert!(time_before <= first_attempt_timestamp); - assert!(first_attempt_timestamp <= second_attempt_timestamp); - assert!(second_attempt_timestamp <= time_after); - assert_eq!(first_attempt_response_skeleton_opt, None); - assert_eq!(second_attempt_response_skeleton_opt, None); - debug!(first_attempt_logger, "first attempt"); - debug!(second_attempt_logger, "second attempt"); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: first attempt")); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: second attempt")); + assert_start_scan_params_after_called_twice( + "receivable", + test_name, + &tlh, + &earning_wallet, + time_before, + time_after, + &start_scan_params_arc, + ); assert_eq!( *notify_later_receivable_params, vec![ @@ -2239,204 +2167,210 @@ mod tests { } #[test] - fn periodical_scanning_for_pending_payable_works() { + fn periodical_scanning_for_payable_works() { init_test_logging(); - let test_name = "periodical_scanning_for_pending_payable_works"; - let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); - let notify_later_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); + let test_name = "periodical_scanning_for_payable_works"; + let begin_pending_payable_scan_params_arc = Arc::new(Mutex::new(vec![])); + let begin_payable_scan_params_arc = Arc::new(Mutex::new(vec![])); + let notify_later_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); + let notify_payable_params_arc = Arc::new(Mutex::new(vec![])); let system = System::new(test_name); - SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge = blockchain_bridge.system_stop_conditions(match_every_type_id!( + RequestTransactionReceipts, + RequestTransactionReceipts, + QualifiedPayablesMessage, + QualifiedPayablesMessage + )); + let blockchain_bridge_addr = blockchain_bridge.start(); let consuming_wallet = make_paying_wallet(b"consuming"); + let pending_payable_fingerprint_1 = make_pending_payable_fingerprint(); + let mut pending_payable_fingerprint_2 = make_pending_payable_fingerprint(); + pending_payable_fingerprint_2.hash = make_tx_hash(987); + let request_transaction_receipts_1 = RequestTransactionReceipts { + pending_payable: vec![pending_payable_fingerprint_1], + response_skeleton_opt: None, + }; + let request_transaction_receipts_2 = RequestTransactionReceipts { + pending_payable: vec![pending_payable_fingerprint_2], + response_skeleton_opt: None + }; let pending_payable_scanner = ScannerMock::new() - .begin_scan_params(&begin_scan_params_arc) - .begin_scan_result(Err(BeginScanError::NothingToProcess)) - .begin_scan_result(Ok(RequestTransactionReceipts { - pending_payable: vec![], + .started_at_result(None) + .started_at_result(None) + .start_scan_params(&begin_pending_payable_scan_params_arc) + .start_scan_result(Err(BeginScanError::NothingToProcess)) + .start_scan_result(Ok(request_transaction_receipts_1)) + .start_scan_result(Ok(request_transaction_receipts_2)); + let payable_scanner = ScannerMock::new() + .started_at_result(None) + .started_at_result(None) + .start_scan_params(&begin_payable_scan_params_arc) + .start_scan_result(Err(BeginScanError::NothingToProcess)) + .start_scan_result(Ok(QualifiedPayablesMessage { + protected_qualified_payables: protect_payables_in_test(vec![make_payable_account( + 123, + )]), + consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: None, - })) - .stop_the_system_after_last_msg(); - let mut config = make_bc_with_defaults(); + })); + let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_secs(100), - receivable_scan_interval: Duration::from_secs(100), - pending_payable_scan_interval: Duration::from_millis(98), + payable_scan_interval: Duration::from_millis(97), + receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner }); let mut subject = AccountantBuilder::default() - .consuming_wallet(consuming_wallet.clone()) .bootstrapper_config(config) + .consuming_wallet(consuming_wallet.clone()) .logger(Logger::new(test_name)) .build(); - subject.scanners.payable = Box::new(NullScanner::new()); //skipping subject.scanners.pending_payable = Box::new(pending_payable_scanner); + subject.scanners.payable = Box::new(payable_scanner); subject.scanners.receivable = Box::new(NullScanner::new()); //skipping - subject.scan_schedulers.update_scheduler( + subject.scan_schedulers.update_periodical_scheduler( ScanType::PendingPayables, Some(Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(¬ify_later_pending_payable_params_arc) + NotifyLaterHandleMock::::default() + .notify_later_params(¬ify_later_pending_payables_params_arc) .capture_msg_and_let_it_fly_on(), )), None, ); - let subject_addr: Addr = subject.start(); + subject.scan_schedulers.update_imminent_scheduler( + ScanType::Payables, + Some(Box::new( + NotifyHandleMock::::default() + .notify_params(¬ify_payable_params_arc) + .capture_msg_and_let_it_fly_on(), + )), + ); + subject.request_transaction_receipts_sub_opt = + Some(blockchain_bridge_addr.clone().recipient()); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); + let subject_addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); - let peer_actors = peer_actors_builder().build(); - send_bind_message!(subject_subs, peer_actors); send_start_message!(subject_subs); let time_before = SystemTime::now(); system.run(); let time_after = SystemTime::now(); - let notify_later_pending_payable_params = - notify_later_pending_payable_params_arc.lock().unwrap(); - TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: There was nothing to process during PendingPayables scan." - )); - let mut begin_scan_params = begin_scan_params_arc.lock().unwrap(); - let ( - first_attempt_wallet, - first_attempt_timestamp, - first_attempt_response_skeleton_opt, - first_attempt_logger, - ) = begin_scan_params.remove(0); - let ( - second_attempt_wallet, - second_attempt_timestamp, - second_attempt_response_skeleton_opt, - second_attempt_logger, - ) = begin_scan_params.remove(0); - assert_eq!(first_attempt_wallet, second_attempt_wallet); - assert_eq!(second_attempt_wallet, consuming_wallet); - assert!(time_before <= first_attempt_timestamp); - assert!(first_attempt_timestamp <= second_attempt_timestamp); - assert!(second_attempt_timestamp <= time_after); - assert_eq!(first_attempt_response_skeleton_opt, None); - assert_eq!(second_attempt_response_skeleton_opt, None); - debug!(first_attempt_logger, "first attempt"); - debug!(second_attempt_logger, "second attempt"); let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: first attempt")); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: second attempt")); + tlh.assert_logs_contain_in_order(vec![ + &format!( + "DEBUG: {test_name}: There was nothing to process during PendingPayables scan." + ), + &format!("DEBUG: {test_name}: There was nothing to process during Payables scan."), + ]); + let (pending_payable_first_attempt_timestamp, pending_payable_second_attempt_timestamp) = + assert_start_scan_params_after_called_twice( + "pending payable", + test_name, + &tlh, + &consuming_wallet, + time_before, + time_after, + &begin_pending_payable_scan_params_arc, + ); + let (payable_first_attempt_timestamp, payable_second_attempt_timestamp) = + assert_start_scan_params_after_called_twice( + "payable", + test_name, + &tlh, + &consuming_wallet, + time_before, + time_after, + &begin_payable_scan_params_arc, + ); + assert!(pending_payable_first_attempt_timestamp < payable_first_attempt_timestamp); + assert!(pending_payable_second_attempt_timestamp < payable_second_attempt_timestamp); + let notify_later_pending_payables_params = + notify_later_pending_payables_params_arc.lock().unwrap(); assert_eq!( - *notify_later_pending_payable_params, + *notify_later_pending_payables_params, vec![ ( ScanForPendingPayables { response_skeleton_opt: None }, - Duration::from_millis(98) + Duration::from_millis(97) + ), + ( + ScanForPendingPayables { + response_skeleton_opt: None + }, + Duration::from_millis(97) ), ( ScanForPendingPayables { response_skeleton_opt: None }, - Duration::from_millis(98) + Duration::from_millis(97) ), ] - ) - } - - #[test] - fn periodical_scanning_for_payable_works() { - init_test_logging(); - let test_name = "periodical_scanning_for_payable_works"; - let begin_scan_params_arc = Arc::new(Mutex::new(vec![])); - let notify_later_payables_params_arc = Arc::new(Mutex::new(vec![])); - let system = System::new(test_name); - SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions - let consuming_wallet = make_paying_wallet(b"consuming"); - let payable_scanner = ScannerMock::new() - .begin_scan_params(&begin_scan_params_arc) - .begin_scan_result(Err(BeginScanError::NothingToProcess)) - .begin_scan_result(Ok(QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(vec![make_payable_account( - 123, - )]), - consuming_wallet: consuming_wallet.clone(), - response_skeleton_opt: None, - })) - .stop_the_system_after_last_msg(); - let mut config = bc_from_earning_wallet(make_wallet("hi")); - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(97), - receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner - pending_payable_scan_interval: Duration::from_secs(100), // We'll never run this scanner - }); - let mut subject = AccountantBuilder::default() - .bootstrapper_config(config) - .consuming_wallet(consuming_wallet.clone()) - .logger(Logger::new(test_name)) - .build(); - subject.scanners.payable = Box::new(payable_scanner); - subject.scanners.pending_payable = Box::new(NullScanner::new()); //skipping - subject.scanners.receivable = Box::new(NullScanner::new()); //skipping - subject.scan_schedulers.update_scheduler( - ScanType::Payables, - Some(Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(¬ify_later_payables_params_arc) - .capture_msg_and_let_it_fly_on(), - )), - None, ); - let subject_addr = subject.start(); - let subject_subs = Accountant::make_subs_from(&subject_addr); - let peer_actors = peer_actors_builder().build(); - send_bind_message!(subject_subs, peer_actors); - - send_start_message!(subject_subs); + let notify_payables_params = notify_payable_params_arc.lock().unwrap(); + assert_eq!( + *notify_payables_params, + vec![ + ScanForPayables { + response_skeleton_opt: None + }, + ScanForPayables { + response_skeleton_opt: None + }, + ScanForPayables { + response_skeleton_opt: None + } + ] + ); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + blockchain_bridge_recording.get_record() + } - let time_before = SystemTime::now(); - system.run(); - let time_after = SystemTime::now(); - //the second attempt is the one where the queue is empty and System::current.stop() ends the cycle - let notify_later_payables_params = notify_later_payables_params_arc.lock().unwrap(); - TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: There was nothing to process during Payables scan." - )); - let mut begin_scan_params = begin_scan_params_arc.lock().unwrap(); + fn assert_start_scan_params_after_called_twice( + scanner_name: &str, + test_name: &str, + tlh: &TestLogHandler, + wallet: &Wallet, + time_before: SystemTime, + time_after: SystemTime, + start_scan_params_arc: &Arc< + Mutex, Logger)>>, + >, + ) -> (SystemTime, SystemTime) { + let mut start_scan_params = start_scan_params_arc.lock().unwrap(); let ( first_attempt_wallet, first_attempt_timestamp, first_attempt_response_skeleton_opt, first_attempt_logger, - ) = begin_scan_params.remove(0); + ) = start_scan_params.remove(0); let ( second_attempt_wallet, second_attempt_timestamp, second_attempt_response_skeleton_opt, second_attempt_logger, - ) = begin_scan_params.remove(0); + ) = start_scan_params.remove(0); assert_eq!(first_attempt_wallet, second_attempt_wallet); - assert_eq!(second_attempt_wallet, consuming_wallet); + assert_eq!(&second_attempt_wallet, wallet); assert!(time_before <= first_attempt_timestamp); assert!(first_attempt_timestamp <= second_attempt_timestamp); assert!(second_attempt_timestamp <= time_after); assert_eq!(first_attempt_response_skeleton_opt, None); assert_eq!(second_attempt_response_skeleton_opt, None); - debug!(first_attempt_logger, "first attempt"); - debug!(second_attempt_logger, "second attempt"); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: first attempt")); - tlh.exists_log_containing(&format!("DEBUG: {test_name}: second attempt")); - assert_eq!( - *notify_later_payables_params, - vec![ - ( - ScanForPayables { - response_skeleton_opt: None - }, - Duration::from_millis(97) - ), - ( - ScanForPayables { - response_skeleton_opt: None - }, - Duration::from_millis(97) - ), - ] - ) + assert!(start_scan_params.is_empty()); + debug!(first_attempt_logger, "first attempt {}", scanner_name); + debug!(second_attempt_logger, "second attempt {}", scanner_name); + tlh.exists_log_containing(&format!( + "DEBUG: {test_name}: first attempt {}", + scanner_name + )); + tlh.exists_log_containing(&format!( + "DEBUG: {test_name}: second attempt {}", + scanner_name + )); + (first_attempt_timestamp, second_attempt_timestamp) } #[test] @@ -2482,7 +2416,6 @@ mod tests { config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(100), receivable_scan_interval: Duration::from_millis(100), - pending_payable_scan_interval: Duration::from_millis(100), }); config.suppress_initial_scans = true; let peer_actors = peer_actors_builder().build(); @@ -2569,12 +2502,9 @@ mod tests { .build(); subject.outbound_payments_instructions_sub_opt = Some(outbound_payments_instructions_sub); - let _result = subject.scanners.payable.begin_scan( - consuming_wallet, - SystemTime::now(), - None, - &subject.logger, - ); + let _result: Result = subject + .scanners + .start_scan_guarded(&consuming_wallet, SystemTime::now(), None, &subject.logger); System::current().stop(); system.run(); @@ -2588,7 +2518,6 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("mine")); let consuming_wallet = make_paying_wallet(b"consuming"); config.scan_intervals_opt = Some(ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(50_000), payable_scan_interval: Duration::from_secs(50_000), receivable_scan_interval: Duration::from_secs(50_000), }); @@ -2624,13 +2553,9 @@ mod tests { let payable_dao = PayableDaoMock::default().non_pending_payables_result(qualified_payables.clone()); let (blockchain_bridge, _, blockchain_bridge_recordings_arc) = make_recorder(); - let blockchain_bridge = blockchain_bridge - .system_stop_conditions(match_every_type_id!(QualifiedPayablesMessage)); + let blockchain_bridge_addr = blockchain_bridge.start(); let system = System::new("scan_for_payable_message_triggers_payment_for_balances_over_the_curve"); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) .consuming_wallet(consuming_wallet.clone()) @@ -2638,12 +2563,15 @@ mod tests { .build(); subject.scanners.pending_payable = Box::new(NullScanner::new()); subject.scanners.receivable = Box::new(NullScanner::new()); - let subject_addr = subject.start(); - let accountant_subs = Accountant::make_subs_from(&subject_addr); - send_bind_message!(accountant_subs, peer_actors); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 31, + context_id: 24, + }); - send_start_message!(accountant_subs); + subject.handle_request_of_scan_for_payable(response_skeleton_opt); + System::current().stop(); system.run(); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); let message = blockchain_bridge_recordings.get_record::(0); @@ -2652,7 +2580,7 @@ mod tests { &QualifiedPayablesMessage { protected_qualified_payables: protect_payables_in_test(qualified_payables), consuming_wallet, - response_skeleton_opt: None, + response_skeleton_opt, } ); } @@ -2661,33 +2589,25 @@ mod tests { fn accountant_does_not_initiate_another_scan_if_one_is_already_running() { init_test_logging(); let test_name = "accountant_does_not_initiate_another_scan_if_one_is_already_running"; - let payable_dao = PayableDaoMock::default(); let (blockchain_bridge, _, blockchain_bridge_recording) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge .system_stop_conditions(match_every_type_id!( - QualifiedPayablesMessage, - QualifiedPayablesMessage + RequestTransactionReceipts, + RequestTransactionReceipts )) .start(); let pps_for_blockchain_bridge_sub = blockchain_bridge_addr.clone().recipient(); - let last_paid_timestamp = to_time_t(SystemTime::now()) - - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec as i64 - - 1; - let payable_account = PayableAccount { - wallet: make_wallet("scan_for_payables"), - balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), - last_paid_timestamp: from_time_t(last_paid_timestamp), - pending_payable_opt: None, - }; - let payable_dao = payable_dao - .non_pending_payables_result(vec![payable_account.clone()]) - .non_pending_payables_result(vec![payable_account]); + let pending_payable_fingerprint = make_pending_payable_fingerprint(); + let pending_payable_dao = PendingPayableDaoMock::new() + .return_all_errorless_fingerprints_result(vec![pending_payable_fingerprint.clone()]) + .increment_scan_attempts_result(Ok(())) + .return_all_errorless_fingerprints_result(vec![pending_payable_fingerprint.clone()]); let config = bc_from_earning_wallet(make_wallet("mine")); let system = System::new(test_name); let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) .logger(Logger::new(test_name)) - .payable_daos(vec![ForPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .bootstrapper_config(config) .build(); let message_before = ScanForPayables { @@ -2702,7 +2622,7 @@ mod tests { context_id: 444, }), }; - subject.qualified_payables_sub_opt = Some(pps_for_blockchain_bridge_sub); + subject.request_transaction_receipts_sub_opt = Some(pps_for_blockchain_bridge_sub); let addr = subject.start(); addr.try_send(message_before.clone()).unwrap(); @@ -2712,34 +2632,35 @@ mod tests { .unwrap(); // We ignored the second ScanForPayables message because the first message meant a scan - // was already in progress; now let's make it look like that scan has ended so that we - // can prove the next message will start another one. - addr.try_send(AssertionsMessage { - assertions: Box::new(|accountant: &mut Accountant| { - accountant - .scanners - .payable - .mark_as_ended(&Logger::new("irrelevant")) - }), + // was already in progress; now let's reset the state by ending the first scan by a failure + // and see that the third scan attempt will be processed willingly again. + addr.try_send(ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::LocalError("bluh".to_string()), + pending_payable_fingerprint, + )], + response_skeleton_opt: None, }) .unwrap(); addr.try_send(message_after.clone()).unwrap(); system.run(); - let recording = blockchain_bridge_recording.lock().unwrap(); - let messages_received = recording.len(); + let blockchain_bridge_recording = blockchain_bridge_recording.lock().unwrap(); + let messages_received = blockchain_bridge_recording.len(); assert_eq!(messages_received, 2); - let first_message: &QualifiedPayablesMessage = recording.get_record(0); + let first_message_actual: &RequestTransactionReceipts = + blockchain_bridge_recording.get_record(0); assert_eq!( - first_message.response_skeleton_opt, + first_message_actual.response_skeleton_opt, message_before.response_skeleton_opt ); - let second_message: &QualifiedPayablesMessage = recording.get_record(1); + let second_message_actual: &RequestTransactionReceipts = + blockchain_bridge_recording.get_record(1); assert_eq!( - second_message.response_skeleton_opt, + second_message_actual.response_skeleton_opt, message_after.response_skeleton_opt ); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {}: Payables scan was already initiated", + "DEBUG: {}: PendingPayables scan was already initiated", test_name )); } @@ -2780,11 +2701,11 @@ mod tests { .bootstrapper_config(config) .build(); - subject.request_transaction_receipts_subs_opt = Some(blockchain_bridge_addr.recipient()); + subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); let account_addr = subject.start(); let _ = account_addr - .try_send(ScanForPendingPayables { + .try_send(ScanForPayables { response_skeleton_opt: None, }) .unwrap(); @@ -3564,7 +3485,7 @@ mod tests { last_paid_timestamp: past_payable_timestamp_2, pending_payable_opt: None, }; - let pending_payable_scan_interval = 1000; // should be slightly less than 1/5 of the time until shutting the system + let payable_scan_interval = 1000; // should be slightly less than 1/5 of the time until shutting the system let payable_dao_for_payable_scanner = PayableDaoMock::new() .non_pending_payables_params(&non_pending_payables_params_arc) .non_pending_payables_result(vec![account_1, account_2]) @@ -3575,9 +3496,8 @@ mod tests { .transactions_confirmed_result(Ok(())); let mut bootstrapper_config = bc_from_earning_wallet(make_wallet("some_wallet_address")); bootstrapper_config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_secs(1_000_000), // we don't care about this scan + payable_scan_interval: Duration::from_millis(payable_scan_interval), // we don't care about this scan receivable_scan_interval: Duration::from_secs(1_000_000), // we don't care about this scan - pending_payable_scan_interval: Duration::from_millis(pending_payable_scan_interval), }); let fingerprint_1_first_round = PendingPayableFingerprint { rowid: rowid_for_account_1, @@ -3687,8 +3607,8 @@ mod tests { let notify_later_half_mock = NotifyLaterHandleMock::default() .notify_later_params(¬ify_later_scan_for_pending_payable_arc_cloned) .capture_msg_and_let_it_fly_on(); - subject.scan_schedulers.update_scheduler( - ScanType::PendingPayables, + subject.scan_schedulers.update_periodical_scheduler( + ScanType::Payables, Some(Box::new(notify_later_half_mock)), None, ); @@ -3744,11 +3664,11 @@ mod tests { *transaction_confirmed_params, vec![vec![fingerprint_2_fourth_round.clone()]] ); - let expected_scan_pending_payable_msg_and_interval = ( - ScanForPendingPayables { + let expected_scan_for_payable_msg_and_interval = ( + ScanForPayables { response_skeleton_opt: None, }, - Duration::from_millis(pending_payable_scan_interval), + Duration::from_millis(payable_scan_interval), ); let mut notify_later_check_for_confirmation = notify_later_scan_for_pending_payable_params_arc @@ -3761,11 +3681,11 @@ mod tests { assert_eq!( vector_of_first_five_cycles, vec![ - expected_scan_pending_payable_msg_and_interval.clone(), - expected_scan_pending_payable_msg_and_interval.clone(), - expected_scan_pending_payable_msg_and_interval.clone(), - expected_scan_pending_payable_msg_and_interval.clone(), - expected_scan_pending_payable_msg_and_interval, + expected_scan_for_payable_msg_and_interval.clone(), + expected_scan_for_payable_msg_and_interval.clone(), + expected_scan_for_payable_msg_and_interval.clone(), + expected_scan_for_payable_msg_and_interval.clone(), + expected_scan_for_payable_msg_and_interval, ] ); let log_handler = TestLogHandler::new(); diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs index 257c88fde..2fa2c0043 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs @@ -8,16 +8,16 @@ pub mod test_utils; use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::Scanner; +use crate::accountant::scanners::AccessibleScanner; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use actix::Message; use itertools::Either; use masq_lib::logger::Logger; -pub trait MultistagePayableScanner: - Scanner + SolvencySensitivePaymentInstructor +pub trait MultistagePayableScanner: + AccessibleScanner + SolvencySensitivePaymentInstructor where - BeginMessage: Message, + StartMessage: Message, EndMessage: Message, { } diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 1307cb006..965a5e0a4 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -19,11 +19,11 @@ use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ }; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport}; use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; -use crate::accountant::PendingPayableId; +use crate::accountant::{PendingPayableId, ScanForPendingPayables}; use crate::accountant::{ comma_joined_stringifiable, gwei_to_wei, Accountant, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForPayables, - ScanForPendingPayables, ScanForReceivables, SentPayables, + ScanForReceivables, SentPayables, }; use crate::accountant::db_access_objects::banned_dao::BannedDao; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; @@ -33,13 +33,13 @@ use crate::sub_lib::accountant::{ use crate::sub_lib::blockchain_bridge::{ OutboundPaymentsInstructions, }; -use crate::sub_lib::utils::{NotifyLaterHandle, NotifyLaterHandleReal}; +use crate::sub_lib::utils::{NotifyHandle, NotifyHandleReal, NotifyLaterHandle, NotifyLaterHandleReal}; use crate::sub_lib::wallet::Wallet; use actix::{Context, Message}; use itertools::{Either, Itertools}; use masq_lib::logger::Logger; use masq_lib::logger::TIME_FORMATTING_STRING; -use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; +use masq_lib::messages::{CommendableScanType, ToMessageBody, UiScanResponse}; use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::cell::RefCell; @@ -56,10 +56,18 @@ use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_le use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::db_config::persistent_configuration::{PersistentConfiguration, PersistentConfigurationReal}; +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum ScanType { + PendingPayables, + Payables, + Receivables, +} + pub struct Scanners { pub payable: Box>, - pub pending_payable: Box>, - pub receivable: Box>, + pub pending_payable: + Box>, + pub receivable: Box>, } impl Scanners { @@ -102,18 +110,114 @@ impl Scanners { } } -pub trait Scanner +pub trait GuardedStartableScanner where - BeginMessage: Message, - EndMessage: Message, + StartMessage: Message, { - fn begin_scan( + fn start_scan_guarded( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result; +} + +impl GuardedStartableScanner for Scanners { + fn start_scan_guarded( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + match ( + self.pending_payable.scan_started_at(), + self.payable.scan_started_at(), + ) { + (Some(pp_timestamp), Some(p_timestamp)) => unreachable!( + "Both payable scanners should never be allowed to run in parallel. Scan for \ + pending payables started at: {}, scan for payables started at: {}", + BeginScanError::timestamp_as_string(pp_timestamp), + BeginScanError::timestamp_as_string(p_timestamp) + ), + (Some(started_at), None) => { + return Err(BeginScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::PendingPayables, + started_at, + }) + } + (None, Some(started_at)) => { + return Err(BeginScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Payables, + started_at, + }) + } + (None, None) => (), + } + self.pending_payable.scan_starter().scanner.start_scan( + wallet, + timestamp, + response_skeleton_opt, + logger, + ) + } +} + +impl GuardedStartableScanner for Scanners { + fn start_scan_guarded( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + if let Some(started_at) = self.payable.scan_started_at() { + unreachable!( + "Guard for pending payables should've prevented running the tandem of scanners if \ + the payable scanner was still running. It started {} and is still running at {}", + BeginScanError::timestamp_as_string(started_at), + BeginScanError::timestamp_as_string(SystemTime::now()) + ) + } + self.payable.scan_starter().scanner.start_scan( + wallet, + timestamp, + response_skeleton_opt, + logger, + ) + } +} + +impl GuardedStartableScanner for Scanners { + fn start_scan_guarded( &mut self, - wallet: Wallet, + wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result; + ) -> Result { + if let Some(started_at) = self.receivable.scan_started_at() { + return Err(BeginScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Receivables, + started_at, + }); + } + self.receivable.scan_starter().scanner.start_scan( + wallet, + timestamp, + response_skeleton_opt, + logger, + ) + } +} + +pub trait AccessibleScanner +where + StartMessage: Message, + EndMessage: Message, +{ + fn scan_starter(&mut self) -> PrivateScanStarter; fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> Option; fn scan_started_at(&self) -> Option; fn mark_as_started(&mut self, timestamp: SystemTime); @@ -123,9 +227,38 @@ where as_any_mut_in_trait!(); } +// Using this Access token to screen away a private interface from its public counterpart, because +// Rust doesn't allow selective private methods within a public trait +pub struct PrivateScanStarter<'s, StartMessage> { + // Strictly private + scanner: &'s mut dyn InaccessibleScanner, +} + +impl<'s, StartMessage> PrivateScanStarter<'s, StartMessage> { + fn new( + scanner: &'s mut dyn InaccessibleScanner, + ) -> PrivateScanStarter<'s, StartMessage> { + Self { scanner } + } +} + +// Strictly private +trait InaccessibleScanner +where + StartMessage: Message, +{ + fn start_scan( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result; +} + pub struct ScannerCommon { initiated_at_opt: Option, - pub payment_thresholds: Rc, + payment_thresholds: Rc, } impl ScannerCommon { @@ -187,17 +320,14 @@ pub struct PayableScanner { pub payment_adjuster: Box, } -impl Scanner for PayableScanner { - fn begin_scan( +impl InaccessibleScanner for PayableScanner { + fn start_scan( &mut self, - consuming_wallet: Wallet, + consuming_wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, ) -> Result { - if let Some(timestamp) = self.scan_started_at() { - return Err(BeginScanError::ScanAlreadyRunning(timestamp)); - } self.mark_as_started(timestamp); info!(logger, "Scanning for payables"); let all_non_pending_payables = self.payable_dao.non_pending_payables(); @@ -225,13 +355,19 @@ impl Scanner for PayableScanner { let protected_payables = self.protect_payables(qualified_payables); let outgoing_msg = QualifiedPayablesMessage::new( protected_payables, - consuming_wallet, + consuming_wallet.clone(), response_skeleton_opt, ); Ok(outgoing_msg) } } } +} + +impl AccessibleScanner for PayableScanner { + fn scan_starter(&mut self) -> PrivateScanStarter { + PrivateScanStarter::new(self) + } fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> Option { let (sent_payables, err_opt) = separate_errors(&message, logger); @@ -564,17 +700,14 @@ pub struct PendingPayableScanner { pub financial_statistics: Rc>, } -impl Scanner for PendingPayableScanner { - fn begin_scan( +impl InaccessibleScanner for PendingPayableScanner { + fn start_scan( &mut self, - _irrelevant_wallet: Wallet, + _wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, ) -> Result { - if let Some(timestamp) = self.scan_started_at() { - return Err(BeginScanError::ScanAlreadyRunning(timestamp)); - } self.mark_as_started(timestamp); info!(logger, "Scanning for pending payable"); let filtered_pending_payable = self.pending_payable_dao.return_all_errorless_fingerprints(); @@ -596,6 +729,14 @@ impl Scanner for PendingP } } } +} + +impl AccessibleScanner + for PendingPayableScanner +{ + fn scan_starter(&mut self) -> PrivateScanStarter { + PrivateScanStarter::new(self) + } fn finish_scan( &mut self, @@ -618,6 +759,7 @@ impl Scanner for PendingP } self.mark_as_ended(logger); + //TODO remove this response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { target: MessageTarget::ClientId(response_skeleton.client_id), body: UiScanResponse {}.tmb(response_skeleton.context_id), @@ -791,26 +933,49 @@ pub struct ReceivableScanner { pub financial_statistics: Rc>, } -impl Scanner for ReceivableScanner { - fn begin_scan( +impl InaccessibleScanner for ReceivableScanner { + fn start_scan( &mut self, - earning_wallet: Wallet, + earning_wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, ) -> Result { - if let Some(timestamp) = self.scan_started_at() { - return Err(BeginScanError::ScanAlreadyRunning(timestamp)); - } self.mark_as_started(timestamp); info!(logger, "Scanning for receivables to {}", earning_wallet); self.scan_for_delinquencies(timestamp, logger); Ok(RetrieveTransactions { - recipient: earning_wallet, + recipient: earning_wallet.clone(), response_skeleton_opt, }) } +} + +impl AccessibleScanner for ReceivableScanner { + // fn start_scan( + // &mut self, + // earning_wallet: Wallet, + // timestamp: SystemTime, + // response_skeleton_opt: Option, + // logger: &Logger, + // ) -> Result { + // if let Some(timestamp) = self.scan_started_at() { + // return Err(BeginScanError::ScanAlreadyRunning(timestamp)); + // } + // self.mark_as_started(timestamp); + // info!(logger, "Scanning for receivables to {}", earning_wallet); + // self.scan_for_delinquencies(timestamp, logger); + // + // Ok(RetrieveTransactions { + // recipient: earning_wallet, + // response_skeleton_opt, + // }) + // } + + fn scan_starter(&mut self) -> PrivateScanStarter { + PrivateScanStarter::new(self) + } fn finish_scan(&mut self, msg: ReceivedPayments, logger: &Logger) -> Option { self.handle_new_received_payments(&msg, logger); @@ -950,7 +1115,10 @@ impl ReceivableScanner { pub enum BeginScanError { NothingToProcess, NoConsumingWalletFound, - ScanAlreadyRunning(SystemTime), + ScanAlreadyRunning { + pertinent_scanner: ScanType, + started_at: SystemTime, + }, CalledFromNullScanner, // Exclusive for tests } @@ -966,11 +1134,12 @@ impl BeginScanError { "There was nothing to process during {:?} scan.", scan_type )), - BeginScanError::ScanAlreadyRunning(timestamp) => Some(format!( - "{:?} scan was already initiated at {}. \ - Hence, this scan request will be ignored.", - scan_type, - BeginScanError::timestamp_as_string(timestamp) + BeginScanError::ScanAlreadyRunning { + pertinent_scanner, + started_at, + } => Some(Self::scan_already_running_msg( + *pertinent_scanner, + *started_at, )), BeginScanError::NoConsumingWalletFound => Some(format!( "Cannot initiate {:?} scan because no consuming wallet was found.", @@ -990,8 +1159,8 @@ impl BeginScanError { } } - fn timestamp_as_string(timestamp: &SystemTime) -> String { - let offset_date_time = OffsetDateTime::from(*timestamp); + fn timestamp_as_string(timestamp: SystemTime) -> String { + let offset_date_time = OffsetDateTime::from(timestamp); offset_date_time .format( &parse(TIME_FORMATTING_STRING) @@ -999,6 +1168,17 @@ impl BeginScanError { ) .expect("Error while formatting timestamp as string.") } + + fn scan_already_running_msg( + error_pertinent_scanner: ScanType, + error_pertinent_scanner_started: SystemTime, + ) -> String { + format!( + "{:?} scan was already initiated at {}. Hence, this scan request will be ignored.", + error_pertinent_scanner, + BeginScanError::timestamp_as_string(error_pertinent_scanner_started) + ) + } } pub struct ScanSchedulers { @@ -1009,19 +1189,18 @@ impl ScanSchedulers { pub fn new(scan_intervals: ScanIntervals) -> Self { let schedulers = HashMap::from_iter([ ( - ScanType::Payables, - Box::new(PeriodicalScanScheduler:: { + ScanType::PendingPayables, + Box::new(PeriodicalScanScheduler:: { handle: Box::new(NotifyLaterHandleReal::default()), interval: scan_intervals.payable_scan_interval, }) as Box, ), ( - ScanType::PendingPayables, - Box::new(PeriodicalScanScheduler:: { - handle: Box::new(NotifyLaterHandleReal::default()), - interval: scan_intervals.pending_payable_scan_interval, - }), - ), + ScanType::Payables, + Box::new(ImminentScanScheduler::{ + handle: Box::new(NotifyHandleReal::default()) + }) + ), ( ScanType::Receivables, Box::new(PeriodicalScanScheduler:: { @@ -1034,34 +1213,323 @@ impl ScanSchedulers { } } -pub struct PeriodicalScanScheduler { - pub handle: Box>, - pub interval: Duration, -} - pub trait ScanScheduler { fn schedule(&self, ctx: &mut Context); - fn interval(&self) -> Duration { - intentionally_blank!() - } - as_any_ref_in_trait!(); as_any_mut_in_trait!(); } -impl ScanScheduler for PeriodicalScanScheduler { +pub struct PeriodicalScanScheduler { + pub handle: Box>, + pub interval: Duration, +} + +impl ScanScheduler for PeriodicalScanScheduler { fn schedule(&self, ctx: &mut Context) { // the default of the message implies response_skeleton_opt to be None // because scheduled scans don't respond - let _ = self.handle.notify_later(T::default(), self.interval, ctx); - } - fn interval(&self) -> Duration { - self.interval + let _ = self + .handle + .notify_later(Message::default(), self.interval, ctx); } + as_any_ref_in_trait_impl!(); + as_any_mut_in_trait_impl!(); +} +pub struct ImminentScanScheduler { + pub handle: Box>, +} + +impl ScanScheduler for ImminentScanScheduler { + fn schedule(&self, ctx: &mut Context) { + // the default of the message implies response_skeleton_opt to be None + // because scheduled scans don't respond + let _ = self.handle.notify(Message::default(), ctx); + } as_any_ref_in_trait_impl!(); as_any_mut_in_trait_impl!(); } + +#[cfg(test)] +pub mod local_test_utils { + use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; + use crate::accountant::scanners::{ + AccessibleScanner, BeginScanError, ImminentScanScheduler, InaccessibleScanner, + MultistagePayableScanner, PeriodicalScanScheduler, PreparedAdjustment, PrivateScanStarter, + ScanSchedulers, ScanType, SolvencySensitivePaymentInstructor, + }; + use crate::accountant::BlockchainAgentWithContextMessage; + use crate::accountant::OutboundPaymentsInstructions; + use crate::accountant::{Accountant, ResponseSkeleton, SentPayables}; + use crate::sub_lib::utils::{NotifyHandle, NotifyLaterHandle}; + use crate::sub_lib::wallet::Wallet; + use actix::{Message, System}; + use itertools::Either; + use masq_lib::logger::Logger; + use masq_lib::messages::CommendableScanType; + use masq_lib::ui_gateway::NodeToUiMessage; + use std::cell::RefCell; + use std::sync::{Arc, Mutex}; + use std::time::{Duration, SystemTime}; + + macro_rules! formal_traits_for_payable_mid_scan_msg_handling { + ($scanner:ty) => { + impl MultistagePayableScanner for $scanner {} + + impl SolvencySensitivePaymentInstructor for $scanner { + fn try_skipping_payment_adjustment( + &self, + _msg: BlockchainAgentWithContextMessage, + _logger: &Logger, + ) -> Result, String> { + intentionally_blank!() + } + + fn perform_payment_adjustment( + &self, + _setup: PreparedAdjustment, + _logger: &Logger, + ) -> OutboundPaymentsInstructions { + intentionally_blank!() + } + } + }; + } + + pub struct NullScanner {} + + impl AccessibleScanner for NullScanner + where + StartMessage: Message, + EndMessage: Message, + { + fn scan_starter(&mut self) -> PrivateScanStarter { + PrivateScanStarter::new(self) + } + + fn finish_scan( + &mut self, + _message: EndMessage, + _logger: &Logger, + ) -> Option { + panic!("Called finish_scan() from NullScanner"); + } + + fn scan_started_at(&self) -> Option { + None + } + + fn mark_as_started(&mut self, _timestamp: SystemTime) { + panic!("Called mark_as_started() from NullScanner"); + } + + fn mark_as_ended(&mut self, _logger: &Logger) { + panic!("Called mark_as_ended() from NullScanner"); + } + + as_any_ref_in_trait_impl!(); + } + + formal_traits_for_payable_mid_scan_msg_handling!(NullScanner); + + impl InaccessibleScanner for NullScanner + where + StartMessage: Message, + { + fn start_scan( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + Err(BeginScanError::CalledFromNullScanner) + } + } + + impl Default for NullScanner { + fn default() -> Self { + Self::new() + } + } + + impl NullScanner { + pub fn new() -> Self { + Self {} + } + } + + pub struct ScannerMock { + start_scan_params: Arc, Logger)>>>, + start_scan_results: RefCell>>, + end_scan_params: Arc>>, + end_scan_results: RefCell>>, + started_at_results: RefCell>>, + stop_system_after_last_message: RefCell, + } + + impl InaccessibleScanner + for ScannerMock + where + StartMessage: Message, + EndMessage: Message, + { + fn start_scan( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + self.start_scan_params.lock().unwrap().push(( + wallet.clone(), + timestamp, + response_skeleton_opt, + logger.clone(), + )); + if self.is_allowed_to_stop_the_system() && self.is_last_message() { + System::current().stop(); + } + self.start_scan_results.borrow_mut().remove(0) + } + } + + impl AccessibleScanner + for ScannerMock + where + StartMessage: Message, + EndMessage: Message, + { + fn scan_starter(&mut self) -> PrivateScanStarter { + PrivateScanStarter::new(self) + } + + fn finish_scan( + &mut self, + message: EndMessage, + _logger: &Logger, + ) -> Option { + self.end_scan_params.lock().unwrap().push(message); + if self.is_allowed_to_stop_the_system() && self.is_last_message() { + System::current().stop(); + } + self.end_scan_results.borrow_mut().remove(0) + } + + fn scan_started_at(&self) -> Option { + self.started_at_results.borrow_mut().remove(0) + } + + fn mark_as_started(&mut self, _timestamp: SystemTime) { + intentionally_blank!() + } + + fn mark_as_ended(&mut self, _logger: &Logger) { + intentionally_blank!() + } + } + + impl Default for ScannerMock { + fn default() -> Self { + Self::new() + } + } + + impl ScannerMock { + pub fn new() -> Self { + Self { + start_scan_params: Arc::new(Mutex::new(vec![])), + start_scan_results: RefCell::new(vec![]), + end_scan_params: Arc::new(Mutex::new(vec![])), + end_scan_results: RefCell::new(vec![]), + started_at_results: RefCell::new(vec![]), + stop_system_after_last_message: RefCell::new(false), + } + } + + pub fn start_scan_params( + mut self, + params: &Arc, Logger)>>>, + ) -> Self { + self.start_scan_params = params.clone(); + self + } + + pub fn start_scan_result(self, result: Result) -> Self { + self.start_scan_results.borrow_mut().push(result); + self + } + + pub fn started_at_result(self, result: Option) -> Self { + self.started_at_results.borrow_mut().push(result); + self + } + + pub fn stop_the_system_after_last_msg(self) -> Self { + self.stop_system_after_last_message.replace(true); + self + } + + pub fn is_allowed_to_stop_the_system(&self) -> bool { + *self.stop_system_after_last_message.borrow() + } + + pub fn is_last_message(&self) -> bool { + self.is_last_message_from_start_scan() || self.is_last_message_from_end_scan() + } + + pub fn is_last_message_from_start_scan(&self) -> bool { + self.start_scan_results.borrow().len() == 1 && self.end_scan_results.borrow().is_empty() + } + + pub fn is_last_message_from_end_scan(&self) -> bool { + self.end_scan_results.borrow().len() == 1 && self.start_scan_results.borrow().is_empty() + } + } + + formal_traits_for_payable_mid_scan_msg_handling!(ScannerMock); + + impl ScanSchedulers { + pub fn update_periodical_scheduler( + &mut self, + scan_type: ScanType, + handle_opt: Option>>, + interval_opt: Option, + ) { + let scheduler: &mut PeriodicalScanScheduler = self.scheduler_mut(scan_type); + if let Some(new_handle) = handle_opt { + scheduler.handle = new_handle + } + if let Some(new_interval) = interval_opt { + scheduler.interval = new_interval + } + } + + pub fn update_imminent_scheduler( + &mut self, + scan_type: ScanType, + handle_opt: Option>>, + ) { + let scheduler: &mut ImminentScanScheduler = self.scheduler_mut(scan_type); + if let Some(new_handle) = handle_opt { + scheduler.handle = new_handle + } + } + + fn scheduler_mut( + &mut self, + scan_type: ScanType, + ) -> &mut SchedulerType { + self.schedulers + .get_mut(&scan_type) + .unwrap() + .as_any_mut() + .downcast_mut::() + .unwrap() + } + } +} + #[cfg(test)] mod tests { use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDaoError}; @@ -1073,19 +1541,9 @@ mod tests { use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PendingPayableMetadata; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; use crate::accountant::scanners::test_utils::protect_payables_in_test; - use crate::accountant::scanners::{ - BeginScanError, PayableScanner, PendingPayableScanner, ReceivableScanner, ScanSchedulers, - Scanner, ScannerCommon, Scanners, - }; - use crate::accountant::test_utils::{ - make_custom_payment_thresholds, make_payable_account, make_payables, - make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, - BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, - PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, - PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, - ReceivableDaoMock, ReceivableScannerBuilder, - }; - use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; + use crate::accountant::scanners::{BeginScanError, PayableScanner, PendingPayableScanner, ReceivableScanner, ScanSchedulers, ScanType, AccessibleScanner, ScannerCommon, Scanners, InaccessibleScanner, GuardedStartableScanner, PeriodicalScanScheduler, ImminentScanScheduler}; + use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; + use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanForPayables, ScanForPendingPayables, ScanForReceivables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ @@ -1106,11 +1564,11 @@ mod tests { use actix::{Message, System}; use ethereum_types::U64; use masq_lib::logger::Logger; - use masq_lib::messages::ScanType; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use regex::Regex; + use regex::{CaptureMatches, Regex}; use rusqlite::{ffi, ErrorCode}; use std::cell::RefCell; + use std::cmp::Ordering; use std::collections::HashSet; use std::ops::Sub; use std::panic::{catch_unwind, AssertUnwindSafe}; @@ -1119,6 +1577,7 @@ mod tests { use std::time::{Duration, SystemTime}; use web3::types::{TransactionReceipt, H256}; use web3::Error; + use crate::accountant::scanners::local_test_utils::NullScanner; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; #[test] @@ -1241,14 +1700,16 @@ mod tests { make_payables(now, &PaymentThresholds::default()); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); - let mut subject = PayableScannerBuilder::new() + let mut subject = make_dull_subject(); + let payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) .build(); + subject.payable = Box::new(payable_scanner); - let result = - subject.begin_scan(consuming_wallet.clone(), now, None, &Logger::new(test_name)); + let result: Result = + subject.start_scan_guarded(&consuming_wallet, now, None, &Logger::new(test_name)); - let timestamp = subject.scan_started_at(); + let timestamp = subject.payable.scan_started_at(); assert_eq!(timestamp, Some(now)); assert_eq!( result, @@ -1270,30 +1731,129 @@ mod tests { } #[test] - fn payable_scanner_throws_error_when_a_scan_is_already_running() { + fn payable_scanner_panics_if_requested_when_already_running() { let consuming_wallet = make_paying_wallet(b"consuming wallet"); - let now = SystemTime::now(); - let (_, _, all_non_pending_payables) = make_payables(now, &PaymentThresholds::default()); + let (_, _, all_non_pending_payables) = + make_payables(SystemTime::now(), &PaymentThresholds::default()); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); - let mut subject = PayableScannerBuilder::new() + let mut subject = make_dull_subject(); + let payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) .build(); - let _result = subject.begin_scan(consuming_wallet.clone(), now, None, &Logger::new("test")); - - let run_again_result = subject.begin_scan( - consuming_wallet, + subject.payable = Box::new(payable_scanner); + let before = SystemTime::now(); + let _: Result = subject.start_scan_guarded( + &consuming_wallet, SystemTime::now(), None, &Logger::new("test"), ); - let is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(is_scan_running, true); - assert_eq!( - run_again_result, - Err(BeginScanError::ScanAlreadyRunning(now)) + let caught_panic = catch_unwind(AssertUnwindSafe(|| { + let _: Result = subject.start_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + ); + })) + .unwrap_err(); + + let after = SystemTime::now(); + let panic_msg = caught_panic.downcast_ref::().unwrap(); + let expected_needle_1 = "internal error: entered unreachable code: Guard for pending \ + payables should've prevented running the tandem of scanners if the payable scanner was \ + still running. It started "; + assert!( + panic_msg.contains(expected_needle_1), + "We looked for {} but the actual string doesn't contain it: {}", + expected_needle_1, + panic_msg ); + let expected_needle_2 = "and is still running at "; + assert!( + panic_msg.contains(expected_needle_2), + "We looked for {} but the actual string doesn't contain it: {}", + expected_needle_2, + panic_msg + ); + let regex = PseudoTimestamp::regex(); + let mut captures = regex.captures_iter(panic_msg); + let first_actual_as_pseudo_timestamp = PseudoTimestamp::new_from_captures(&mut captures); + let second_actual_as_pseudo_timestamp = PseudoTimestamp::new_from_captures(&mut captures); + let before_as_pseudo_timestamp = PseudoTimestamp::from(before); + let after_as_pseudo_timestamp = PseudoTimestamp::from(after); + assert!( + before_as_pseudo_timestamp <= first_actual_as_pseudo_timestamp + && first_actual_as_pseudo_timestamp <= second_actual_as_pseudo_timestamp + && second_actual_as_pseudo_timestamp <= after_as_pseudo_timestamp, + "We expected this relationship before({:?}) <= first_actual({:?}) <= second_actual({:?}) \ + <= after({:?}), but it does not hold true", + before_as_pseudo_timestamp, + first_actual_as_pseudo_timestamp, + second_actual_as_pseudo_timestamp, + after_as_pseudo_timestamp + ); + } + + // Concatenated hours, minutes, seconds and milliseconds in a single integer + #[derive(PartialEq, Debug)] + struct PseudoTimestamp { + rep: u32, + } + + // This is a on-hour difference, indicating wrapping around the midnight + const MIDNIGHT_INDICATIVE_DIFFERENCE_DIFF: u32 = 1_000_000; + + impl PartialOrd for PseudoTimestamp { + fn partial_cmp(&self, other: &Self) -> Option { + if self.rep == other.rep { + Some(Ordering::Equal) + } else if self.rep < other.rep { + if (other.rep - self.rep) > MIDNIGHT_INDICATIVE_DIFFERENCE_DIFF { + Some(Ordering::Greater) + } else { + Some(Ordering::Less) + } + } else { + if (self.rep - other.rep) > MIDNIGHT_INDICATIVE_DIFFERENCE_DIFF { + Some(Ordering::Less) + } else { + Some(Ordering::Greater) + } + } + } + } + + impl From for PseudoTimestamp { + fn from(timestamp: SystemTime) -> Self { + let specially_formatted_timestamp = BeginScanError::timestamp_as_string(timestamp); + let regex = Self::regex(); + let mut captures = regex.captures_iter(&specially_formatted_timestamp); + PseudoTimestamp::new_from_captures(&mut captures) + } + } + + impl PseudoTimestamp { + fn new_from_captures(captures: &mut CaptureMatches) -> Self { + let captured_first_time = captures.next().unwrap().get(1).unwrap().as_str(); + let num = Self::remove_colons_and_dots(captured_first_time); + Self { + rep: u32::from_str_radix(&num, 10).unwrap(), + } + } + + fn regex() -> Regex { + Regex::new(r"\d{4}-\d{2}-\d{2} (\d{2}:\d{2}:\d{2}\.\d{3})").unwrap() + } + + fn remove_colons_and_dots(str: &str) -> String { + let mut str = str.to_string(); + str = str.replace(":", ""); + str = str.replace(".", ""); + str + } } #[test] @@ -1308,7 +1868,12 @@ mod tests { .payable_dao(payable_dao) .build(); - let result = subject.begin_scan(consuming_wallet, now, None, &Logger::new("test")); + let result = subject.scan_starter().scanner.start_scan( + &consuming_wallet, + now, + None, + &Logger::new("test"), + ); let is_scan_running = subject.scan_started_at().is_some(); assert_eq!(is_scan_running, false); @@ -2211,19 +2776,19 @@ mod tests { let fingerprints = vec![payable_fingerprint_1, payable_fingerprint_2]; let pending_payable_dao = PendingPayableDaoMock::new() .return_all_errorless_fingerprints_result(fingerprints.clone()); - let mut pending_payable_scanner = PendingPayableScannerBuilder::new() + let mut subject = make_dull_subject(); + let pending_payable_scanner = PendingPayableScannerBuilder::new() .pending_payable_dao(pending_payable_dao) .build(); + subject.pending_payable = Box::new(pending_payable_scanner); + let payable_scanner = PayableScannerBuilder::new().build(); + subject.payable = Box::new(payable_scanner); - let result = pending_payable_scanner.begin_scan( - consuming_wallet, - now, - None, - &Logger::new(test_name), - ); + let result: Result = + subject.start_scan_guarded(&consuming_wallet, now, None, &Logger::new(test_name)); let no_of_pending_payables = fingerprints.len(); - let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); + let is_scan_running = subject.pending_payable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); assert_eq!( result, @@ -2241,29 +2806,122 @@ mod tests { } #[test] - fn pending_payable_scanner_throws_error_in_case_scan_is_already_running() { + fn pending_payable_scanner_cannot_be_initiated_if_it_itself_is_already_running() { let now = SystemTime::now(); let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = make_dull_subject(); let pending_payable_dao = PendingPayableDaoMock::new() - .return_all_errorless_fingerprints_result(vec![PendingPayableFingerprint { - rowid: 1234, - timestamp: SystemTime::now(), - hash: make_tx_hash(1), - attempt: 1, - amount: 1_000_000, - process_error: None, - }]); - let mut subject = PendingPayableScannerBuilder::new() + .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]); + let pending_payable_scanner = PendingPayableScannerBuilder::new() .pending_payable_dao(pending_payable_dao) .build(); + subject.pending_payable = Box::new(pending_payable_scanner); + let payable_scanner = PayableScannerBuilder::new().build(); + subject.payable = Box::new(payable_scanner); let logger = Logger::new("test"); - let _ = subject.begin_scan(consuming_wallet.clone(), now, None, &logger); + let _: Result = + subject.start_scan_guarded(&consuming_wallet, now, None, &logger); - let result = subject.begin_scan(consuming_wallet, SystemTime::now(), None, &logger); + let result: Result = + subject.start_scan_guarded(&consuming_wallet, SystemTime::now(), None, &logger); - let is_scan_running = subject.scan_started_at().is_some(); + let is_scan_running = subject.pending_payable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); - assert_eq!(result, Err(BeginScanError::ScanAlreadyRunning(now))); + assert_eq!( + result, + Err(BeginScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::PendingPayables, + started_at: now + }) + ); + } + + #[test] + fn pending_payable_scanner_cannot_be_initiated_if_payable_scanner_is_still_running() { + let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = make_dull_subject(); + let pending_payable_scanner = PendingPayableScannerBuilder::new().build(); + let payable_scanner = PayableScannerBuilder::new().build(); + subject.pending_payable = Box::new(pending_payable_scanner); + subject.payable = Box::new(payable_scanner); + let logger = Logger::new("test"); + let previous_scan_started_at = SystemTime::now(); + subject.payable.mark_as_started(previous_scan_started_at); + + let result: Result = + subject.start_scan_guarded(&consuming_wallet, SystemTime::now(), None, &logger); + + let is_scan_running = subject.pending_payable.scan_started_at().is_some(); + assert_eq!(is_scan_running, false); + assert_eq!( + result, + Err(BeginScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Payables, + started_at: previous_scan_started_at + }) + ); + } + + #[test] + fn both_payable_scanners_cannot_be_detected_in_progress_at_the_same_time() { + let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = make_dull_subject(); + let pending_payable_scanner = PendingPayableScannerBuilder::new().build(); + let payable_scanner = PayableScannerBuilder::new().build(); + subject.pending_payable = Box::new(pending_payable_scanner); + subject.payable = Box::new(payable_scanner); + let timestamp_pending_payable_start = SystemTime::now() + .checked_sub(Duration::from_millis(12)) + .unwrap(); + let timestamp_payable_scanner_start = SystemTime::now(); + subject + .pending_payable + .mark_as_started(timestamp_pending_payable_start); + subject + .payable + .mark_as_started(timestamp_payable_scanner_start); + + let caught_panic = catch_unwind(AssertUnwindSafe(|| { + let _: Result = subject.start_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + ); + })) + .unwrap_err(); + + let panic_msg = caught_panic.downcast_ref::().unwrap(); + let expected_msg_fragment_1 = "internal error: entered unreachable code: Both payable \ + scanners should never be allowed to run in parallel. Scan for pending payables started at: "; + assert!( + panic_msg.contains(expected_msg_fragment_1), + "This fragment '{}' wasn't found in \ + '{}'", + expected_msg_fragment_1, + panic_msg + ); + let expected_msg_fragment_2 = ", scan for payables started at: "; + assert!( + panic_msg.contains(expected_msg_fragment_2), + "This fragment '{}' wasn't found in \ + '{}'", + expected_msg_fragment_2, + panic_msg + ); + let regex = PseudoTimestamp::regex(); + let mut captures = regex.captures_iter(panic_msg); + let pseudo_timestamp_for_pending_payable_start = + PseudoTimestamp::new_from_captures(&mut captures); + let pseudo_timestamp_for_payable_start = PseudoTimestamp::new_from_captures(&mut captures); + assert_eq!( + pseudo_timestamp_for_pending_payable_start, + PseudoTimestamp::from(timestamp_pending_payable_start) + ); + assert_eq!( + pseudo_timestamp_for_payable_start, + PseudoTimestamp::from(timestamp_payable_scanner_start) + ) } #[test] @@ -2277,7 +2935,7 @@ mod tests { .build(); let result = - pending_payable_scanner.begin_scan(consuming_wallet, now, None, &Logger::new("test")); + pending_payable_scanner.start_scan(&consuming_wallet, now, None, &Logger::new("test")); let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); assert_eq!(result, Err(BeginScanError::NothingToProcess)); @@ -2906,18 +3564,16 @@ mod tests { .new_delinquencies_result(vec![]) .paid_delinquencies_result(vec![]); let earning_wallet = make_wallet("earning"); - let mut receivable_scanner = ReceivableScannerBuilder::new() + let mut subject = make_dull_subject(); + let receivable_scanner = ReceivableScannerBuilder::new() .receivable_dao(receivable_dao) .build(); + subject.receivable = Box::new(receivable_scanner); - let result = receivable_scanner.begin_scan( - earning_wallet.clone(), - now, - None, - &Logger::new(test_name), - ); + let result: Result = + subject.start_scan_guarded(&earning_wallet, now, None, &Logger::new(test_name)); - let is_scan_running = receivable_scanner.scan_started_at().is_some(); + let is_scan_running = subject.receivable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); assert_eq!( result, @@ -2938,22 +3594,30 @@ mod tests { .new_delinquencies_result(vec![]) .paid_delinquencies_result(vec![]); let earning_wallet = make_wallet("earning"); - let mut receivable_scanner = ReceivableScannerBuilder::new() + let mut subject = make_dull_subject(); + let receivable_scanner = ReceivableScannerBuilder::new() .receivable_dao(receivable_dao) .build(); - let _ = - receivable_scanner.begin_scan(earning_wallet.clone(), now, None, &Logger::new("test")); + subject.receivable = Box::new(receivable_scanner); + let _: Result = + subject.start_scan_guarded(&earning_wallet, now, None, &Logger::new("test")); - let result = receivable_scanner.begin_scan( - earning_wallet, + let result: Result = subject.start_scan_guarded( + &earning_wallet, SystemTime::now(), None, &Logger::new("test"), ); - let is_scan_running = receivable_scanner.scan_started_at().is_some(); + let is_scan_running = subject.receivable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); - assert_eq!(result, Err(BeginScanError::ScanAlreadyRunning(now))); + assert_eq!( + result, + Err(BeginScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Receivables, + started_at: now + }) + ); } #[test] @@ -2986,7 +3650,7 @@ mod tests { let logger = Logger::new("DELINQUENCY_TEST"); let now = SystemTime::now(); - let result = receivable_scanner.begin_scan(earning_wallet.clone(), now, None, &logger); + let result = receivable_scanner.start_scan(&earning_wallet, now, None, &logger); assert_eq!( result, @@ -3304,7 +3968,7 @@ mod tests { } fn assert_elapsed_time_in_mark_as_ended( - subject: &mut dyn Scanner, + subject: &mut dyn AccessibleScanner, scanner_name: &str, test_name: &str, logger: &Logger, @@ -3366,39 +4030,82 @@ mod tests { ); } + #[test] + fn scan_already_running_msg_displays_correctly() { + let still_running_scanner = ScanType::PendingPayables; + let time = SystemTime::now(); + + let result = BeginScanError::scan_already_running_msg(still_running_scanner, time); + + let expected_first_fragment = "PendingPayables scan was already initiated at"; + assert!( + result.contains(expected_first_fragment), + "We expected {} but the msg is: {}", + expected_first_fragment, + result + ); + let expected_second_fragment = ". Hence, this scan request will be ignored."; + assert!( + result.contains(expected_second_fragment), + "We expected {} but the msg is: {}", + expected_second_fragment, + result + ); + let regex = PseudoTimestamp::regex(); + let mut captures = regex.captures_iter(&result); + let pseudo_timestamp_for_pending_payable_start = + PseudoTimestamp::new_from_captures(&mut captures); + assert_eq!( + pseudo_timestamp_for_pending_payable_start, + PseudoTimestamp::from(time) + ); + } + #[test] fn scan_schedulers_can_be_properly_initialized() { let scan_intervals = ScanIntervals { payable_scan_interval: Duration::from_secs(240), - pending_payable_scan_interval: Duration::from_secs(300), receivable_scan_interval: Duration::from_secs(360), }; let result = ScanSchedulers::new(scan_intervals); - assert_eq!( - result - .schedulers - .get(&ScanType::Payables) - .unwrap() - .interval(), - scan_intervals.payable_scan_interval - ); assert_eq!( result .schedulers .get(&ScanType::PendingPayables) .unwrap() - .interval(), - scan_intervals.pending_payable_scan_interval + .as_any() + .downcast_ref::>() + .unwrap() + .interval, + scan_intervals.payable_scan_interval ); + result + .schedulers + .get(&ScanType::Payables) + .unwrap() + .as_any() + .downcast_ref::>() + .unwrap(); assert_eq!( result .schedulers .get(&ScanType::Receivables) .unwrap() - .interval(), + .as_any() + .downcast_ref::>() + .unwrap() + .interval, scan_intervals.receivable_scan_interval ); } + + fn make_dull_subject() -> Scanners { + Scanners { + payable: Box::new(NullScanner::new()), + pending_payable: Box::new(NullScanner::new()), + receivable: Box::new(NullScanner::new()), + } + } } diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 5c9d6f14a..0e928fa29 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -14,20 +14,11 @@ use crate::accountant::db_access_objects::receivable_dao::{ }; use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdjuster}; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{ - BlockchainAgentWithContextMessage, QualifiedPayablesMessage, -}; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::{ - MultistagePayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, -}; +use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::PreparedAdjustment; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; -use crate::accountant::scanners::{ - BeginScanError, PayableScanner, PendingPayableScanner, PeriodicalScanScheduler, - ReceivableScanner, ScanSchedulers, Scanner, -}; -use crate::accountant::{ - gwei_to_wei, Accountant, ResponseSkeleton, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC, -}; +use crate::accountant::scanners::{PayableScanner, PendingPayableScanner, ReceivableScanner}; +use crate::accountant::{gwei_to_wei, Accountant, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; @@ -39,17 +30,13 @@ use crate::db_config::mocks::ConfigDaoMock; use crate::sub_lib::accountant::{DaoFactories, FinancialStatistics}; use crate::sub_lib::accountant::{MessageIdGenerator, PaymentThresholds}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; -use crate::sub_lib::utils::NotifyLaterHandle; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::unshared_test_utils::make_bc_with_defaults; -use actix::{Message, System}; +use actix::System; use ethereum_types::H256; -use itertools::Either; use masq_lib::logger::Logger; -use masq_lib::messages::ScanType; -use masq_lib::ui_gateway::NodeToUiMessage; use rusqlite::{Connection, OpenFlags, Row}; use std::any::type_name; use std::cell::RefCell; @@ -57,7 +44,7 @@ use std::fmt::Debug; use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex}; -use std::time::{Duration, SystemTime}; +use std::time::SystemTime; pub fn make_receivable_account(n: u64, expected_delinquent: bool) -> ReceivableAccount { let now = to_time_t(SystemTime::now()); @@ -1508,208 +1495,3 @@ impl PaymentAdjusterMock { self } } - -macro_rules! formal_traits_for_payable_mid_scan_msg_handling { - ($scanner:ty) => { - impl MultistagePayableScanner for $scanner {} - - impl SolvencySensitivePaymentInstructor for $scanner { - fn try_skipping_payment_adjustment( - &self, - _msg: BlockchainAgentWithContextMessage, - _logger: &Logger, - ) -> Result, String> { - intentionally_blank!() - } - - fn perform_payment_adjustment( - &self, - _setup: PreparedAdjustment, - _logger: &Logger, - ) -> OutboundPaymentsInstructions { - intentionally_blank!() - } - } - }; -} - -pub struct NullScanner {} - -impl Scanner for NullScanner -where - BeginMessage: Message, - EndMessage: Message, -{ - fn begin_scan( - &mut self, - _wallet_opt: Wallet, - _timestamp: SystemTime, - _response_skeleton_opt: Option, - _logger: &Logger, - ) -> Result { - Err(BeginScanError::CalledFromNullScanner) - } - - fn finish_scan(&mut self, _message: EndMessage, _logger: &Logger) -> Option { - panic!("Called finish_scan() from NullScanner"); - } - - fn scan_started_at(&self) -> Option { - panic!("Called scan_started_at() from NullScanner"); - } - - fn mark_as_started(&mut self, _timestamp: SystemTime) { - panic!("Called mark_as_started() from NullScanner"); - } - - fn mark_as_ended(&mut self, _logger: &Logger) { - panic!("Called mark_as_ended() from NullScanner"); - } - - as_any_ref_in_trait_impl!(); -} - -formal_traits_for_payable_mid_scan_msg_handling!(NullScanner); - -impl Default for NullScanner { - fn default() -> Self { - Self::new() - } -} - -impl NullScanner { - pub fn new() -> Self { - Self {} - } -} - -pub struct ScannerMock { - begin_scan_params: Arc, Logger)>>>, - begin_scan_results: RefCell>>, - end_scan_params: Arc>>, - end_scan_results: RefCell>>, - stop_system_after_last_message: RefCell, -} - -impl Scanner - for ScannerMock -where - BeginMessage: Message, - EndMessage: Message, -{ - fn begin_scan( - &mut self, - wallet: Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, - ) -> Result { - self.begin_scan_params.lock().unwrap().push(( - wallet, - timestamp, - response_skeleton_opt, - logger.clone(), - )); - if self.is_allowed_to_stop_the_system() && self.is_last_message() { - System::current().stop(); - } - self.begin_scan_results.borrow_mut().remove(0) - } - - fn finish_scan(&mut self, message: EndMessage, _logger: &Logger) -> Option { - self.end_scan_params.lock().unwrap().push(message); - if self.is_allowed_to_stop_the_system() && self.is_last_message() { - System::current().stop(); - } - self.end_scan_results.borrow_mut().remove(0) - } - - fn scan_started_at(&self) -> Option { - intentionally_blank!() - } - - fn mark_as_started(&mut self, _timestamp: SystemTime) { - intentionally_blank!() - } - - fn mark_as_ended(&mut self, _logger: &Logger) { - intentionally_blank!() - } -} - -impl Default for ScannerMock { - fn default() -> Self { - Self::new() - } -} - -impl ScannerMock { - pub fn new() -> Self { - Self { - begin_scan_params: Arc::new(Mutex::new(vec![])), - begin_scan_results: RefCell::new(vec![]), - end_scan_params: Arc::new(Mutex::new(vec![])), - end_scan_results: RefCell::new(vec![]), - stop_system_after_last_message: RefCell::new(false), - } - } - - pub fn begin_scan_params( - mut self, - params: &Arc, Logger)>>>, - ) -> Self { - self.begin_scan_params = params.clone(); - self - } - - pub fn begin_scan_result(self, result: Result) -> Self { - self.begin_scan_results.borrow_mut().push(result); - self - } - - pub fn stop_the_system_after_last_msg(self) -> Self { - self.stop_system_after_last_message.replace(true); - self - } - - pub fn is_allowed_to_stop_the_system(&self) -> bool { - *self.stop_system_after_last_message.borrow() - } - - pub fn is_last_message(&self) -> bool { - self.is_last_message_from_begin_scan() || self.is_last_message_from_end_scan() - } - - pub fn is_last_message_from_begin_scan(&self) -> bool { - self.begin_scan_results.borrow().len() == 1 && self.end_scan_results.borrow().is_empty() - } - - pub fn is_last_message_from_end_scan(&self) -> bool { - self.end_scan_results.borrow().len() == 1 && self.begin_scan_results.borrow().is_empty() - } -} - -formal_traits_for_payable_mid_scan_msg_handling!(ScannerMock); - -impl ScanSchedulers { - pub fn update_scheduler( - &mut self, - scan_type: ScanType, - handle_opt: Option>>, - interval_opt: Option, - ) { - let scheduler = self - .schedulers - .get_mut(&scan_type) - .unwrap() - .as_any_mut() - .downcast_mut::>() - .unwrap(); - if let Some(new_handle) = handle_opt { - scheduler.handle = new_handle - } - if let Some(new_interval) = interval_opt { - scheduler.interval = new_interval - } - } -} diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index e4275b036..ddcbd2778 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -36,7 +36,6 @@ use futures::Future; use itertools::Itertools; use masq_lib::blockchains::chains::Chain; use masq_lib::logger::Logger; -use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeFromUiMessage; use regex::Regex; use std::path::Path; @@ -47,6 +46,7 @@ use ethabi::Hash; use web3::types::H256; use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::ScanType; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; pub const CRASH_KEY: &str = "BLOCKCHAINBRIDGE"; @@ -583,7 +583,6 @@ mod tests { use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::System; use ethereum_types::U64; - use masq_lib::messages::ScanType; use masq_lib::test_utils::logging::init_test_logging; use masq_lib::test_utils::logging::TestLogHandler; use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; diff --git a/node/src/daemon/setup_reporter.rs b/node/src/daemon/setup_reporter.rs index 84c1db1e5..4a18f7746 100644 --- a/node/src/daemon/setup_reporter.rs +++ b/node/src/daemon/setup_reporter.rs @@ -3440,10 +3440,10 @@ mod tests { #[test] fn scan_intervals_computed_default_persistent_config_unequal_to_default() { let mut scan_intervals = *DEFAULT_SCAN_INTERVALS; - scan_intervals.pending_payable_scan_interval = scan_intervals - .pending_payable_scan_interval + scan_intervals.payable_scan_interval = scan_intervals + .payable_scan_interval .add(Duration::from_secs(15)); - scan_intervals.pending_payable_scan_interval = scan_intervals + scan_intervals.receivable_scan_interval = scan_intervals .receivable_scan_interval .sub(Duration::from_secs(33)); diff --git a/node/src/db_config/persistent_configuration.rs b/node/src/db_config/persistent_configuration.rs index 532048a34..ed02642f6 100644 --- a/node/src/db_config/persistent_configuration.rs +++ b/node/src/db_config/persistent_configuration.rs @@ -1949,9 +1949,8 @@ mod tests { fn scan_intervals_get_method_works() { persistent_config_plain_data_assertions_for_simple_get_method!( "scan_intervals", - "40|60|50", + "60|50", ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(40), payable_scan_interval: Duration::from_secs(60), receivable_scan_interval: Duration::from_secs(50), } diff --git a/node/src/node_configurator/configurator.rs b/node/src/node_configurator/configurator.rs index 19b0b958a..750da542f 100644 --- a/node/src/node_configurator/configurator.rs +++ b/node/src/node_configurator/configurator.rs @@ -614,7 +614,6 @@ impl Configurator { let routing_service_rate = rate_pack.routing_service_rate; let exit_byte_rate = rate_pack.exit_byte_rate; let exit_service_rate = rate_pack.exit_service_rate; - let pending_payable_sec = scan_intervals.pending_payable_scan_interval.as_secs(); let payable_sec = scan_intervals.payable_scan_interval.as_secs(); let receivable_sec = scan_intervals.receivable_scan_interval.as_secs(); let threshold_interval_sec = payment_thresholds.threshold_interval_sec; @@ -652,7 +651,6 @@ impl Configurator { }, start_block_opt, scan_intervals: UiScanIntervals { - pending_payable_sec, payable_sec, receivable_sec, }, @@ -2591,7 +2589,6 @@ mod tests { }, start_block_opt: Some(3456), scan_intervals: UiScanIntervals { - pending_payable_sec: 122, payable_sec: 125, receivable_sec: 128 } @@ -2610,7 +2607,6 @@ mod tests { exit_service_rate: 13, })) .scan_intervals_result(Ok(ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(122), payable_scan_interval: Duration::from_secs(125), receivable_scan_interval: Duration::from_secs(128), })) @@ -2722,7 +2718,6 @@ mod tests { }, start_block_opt: Some(3456), scan_intervals: UiScanIntervals { - pending_payable_sec: 122, payable_sec: 125, receivable_sec: 128 } @@ -2760,7 +2755,6 @@ mod tests { exit_service_rate: 0, })) .scan_intervals_result(Ok(ScanIntervals { - pending_payable_scan_interval: Default::default(), payable_scan_interval: Default::default(), receivable_scan_interval: Default::default(), })) @@ -2815,7 +2809,6 @@ mod tests { }, start_block_opt: Some(3456), scan_intervals: UiScanIntervals { - pending_payable_sec: 0, payable_sec: 0, receivable_sec: 0 } diff --git a/node/src/node_configurator/unprivileged_parse_args_configuration.rs b/node/src/node_configurator/unprivileged_parse_args_configuration.rs index 4238bd8d5..38d643864 100644 --- a/node/src/node_configurator/unprivileged_parse_args_configuration.rs +++ b/node/src/node_configurator/unprivileged_parse_args_configuration.rs @@ -1827,7 +1827,6 @@ mod tests { let mut persistent_configuration = configure_default_persistent_config(RATE_PACK | MAPPING_PROTOCOL) .scan_intervals_result(Ok(ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(100), payable_scan_interval: Duration::from_secs(101), receivable_scan_interval: Duration::from_secs(102), })) @@ -1855,7 +1854,6 @@ mod tests { .unwrap(); let expected_scan_intervals = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(180), payable_scan_interval: Duration::from_secs(150), receivable_scan_interval: Duration::from_secs(130), }; @@ -1903,7 +1901,6 @@ mod tests { let mut persistent_configuration = configure_default_persistent_config(RATE_PACK | MAPPING_PROTOCOL) .scan_intervals_result(Ok(ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(180), payable_scan_interval: Duration::from_secs(150), receivable_scan_interval: Duration::from_secs(130), })) @@ -1935,7 +1932,6 @@ mod tests { unban_below_gwei: 20000, }; let expected_scan_intervals = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(180), payable_scan_interval: Duration::from_secs(150), receivable_scan_interval: Duration::from_secs(130), }; diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index 4b005f713..a8cc1cdba 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -38,7 +38,6 @@ lazy_static! { unban_below_gwei: 500_000_000, }; pub static ref DEFAULT_SCAN_INTERVALS: ScanIntervals = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(600), payable_scan_interval: Duration::from_secs(600), receivable_scan_interval: Duration::from_secs(600) }; @@ -80,7 +79,6 @@ pub struct DaoFactories { #[derive(PartialEq, Eq, Debug, Clone, Copy)] pub struct ScanIntervals { pub payable_scan_interval: Duration, - pub pending_payable_scan_interval: Duration, pub receivable_scan_interval: Duration, } @@ -231,7 +229,6 @@ mod tests { unban_below_gwei: 500_000_000, }; let scan_intervals_expected = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(600), payable_scan_interval: Duration::from_secs(600), receivable_scan_interval: Duration::from_secs(600), }; diff --git a/node/src/sub_lib/combined_parameters.rs b/node/src/sub_lib/combined_parameters.rs index f70f60f0f..56a2ab013 100644 --- a/node/src/sub_lib/combined_parameters.rs +++ b/node/src/sub_lib/combined_parameters.rs @@ -177,7 +177,6 @@ impl CombinedParams { ScanIntervals, &parsed_values, Duration::from_secs, - "pending_payable_scan_interval", "payable_scan_interval", "receivable_scan_interval" ))) @@ -224,8 +223,7 @@ impl Display for ScanIntervals { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "{}|{}|{}", - self.pending_payable_scan_interval.as_secs(), + "{}|{}", self.payable_scan_interval.as_secs(), self.receivable_scan_interval.as_secs() ) @@ -550,14 +548,13 @@ mod tests { #[test] fn scan_intervals_from_combined_params() { - let scan_intervals_str = "110|115|113"; + let scan_intervals_str = "115|113"; let result = ScanIntervals::try_from(scan_intervals_str).unwrap(); assert_eq!( result, ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(110), payable_scan_interval: Duration::from_secs(115), receivable_scan_interval: Duration::from_secs(113) } @@ -567,14 +564,13 @@ mod tests { #[test] fn scan_intervals_to_combined_params() { let scan_intervals = ScanIntervals { - pending_payable_scan_interval: Duration::from_secs(60), payable_scan_interval: Duration::from_secs(70), receivable_scan_interval: Duration::from_secs(100), }; let result = scan_intervals.to_string(); - assert_eq!(result, "60|70|100".to_string()); + assert_eq!(result, "70|100".to_string()); } #[test] diff --git a/node/src/sub_lib/utils.rs b/node/src/sub_lib/utils.rs index d68d721bb..5bd7a655a 100644 --- a/node/src/sub_lib/utils.rs +++ b/node/src/sub_lib/utils.rs @@ -222,6 +222,7 @@ impl NLSpawnHandleHolder for NLSpawnHandleHolderReal { } } +#[derive(Default)] pub struct NotifyHandleReal { phantom: PhantomData, } diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 1bf32b4b5..4112186b6 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -950,7 +950,7 @@ pub mod unshared_test_utils { self } - pub fn permit_to_send_out(mut self) -> Self { + pub fn capture_msg_and_let_it_fly_on(mut self) -> Self { self.send_message_out = true; self } diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index f66125182..7a8b0acc0 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -5,8 +5,8 @@ use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::B use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; use crate::accountant::ReportTransactionReceipts; use crate::accountant::{ - ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForPayables, - ScanForPendingPayables, ScanForReceivables, SentPayables, + ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForPayables, ScanForReceivables, + SentPayables, }; use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; use crate::blockchain::blockchain_bridge::RetrieveTransactions; @@ -163,7 +163,6 @@ recorder_message_handler_t_m_p!(RequestTransactionReceipts); recorder_message_handler_t_m_p!(RetrieveTransactions); recorder_message_handler_t_m_p!(ScanError); recorder_message_handler_t_m_p!(ScanForPayables); -recorder_message_handler_t_m_p!(ScanForPendingPayables); recorder_message_handler_t_m_p!(ScanForReceivables); recorder_message_handler_t_m_p!(SentPayables); recorder_message_handler_t_m_p!(StartMessage); diff --git a/node/src/test_utils/recorder_stop_conditions.rs b/node/src/test_utils/recorder_stop_conditions.rs index b3dca287d..9e3c92212 100644 --- a/node/src/test_utils/recorder_stop_conditions.rs +++ b/node/src/test_utils/recorder_stop_conditions.rs @@ -155,11 +155,11 @@ macro_rules! match_every_type_id{ } mod tests { + use crate::accountant::scanners::ScanType; use crate::accountant::{ResponseSkeleton, ScanError, ScanForPayables}; use crate::daemon::crash_notification::CrashNotification; use crate::sub_lib::peer_actors::{NewPublicIp, StartMessage}; use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; - use masq_lib::messages::ScanType; use std::any::TypeId; use std::net::{IpAddr, Ipv4Addr}; use std::vec; From 66dd5c5006f621ae5bee76a502dc27e7ace499b9 Mon Sep 17 00:00:00 2001 From: Bert Date: Mon, 7 Apr 2025 23:06:01 +0200 Subject: [PATCH 02/49] GH-602: interim commit --- node/src/accountant/mod.rs | 203 ++++++++++++++++------------ node/src/accountant/scanners/mod.rs | 18 ++- 2 files changed, 130 insertions(+), 91 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 1f56dfba8..04214c148 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -134,6 +134,12 @@ pub struct ReceivedPayments { pub response_skeleton_opt: Option, } +#[derive(Debug, PartialEq, Eq, Message, Clone)] +pub struct ReportTransactionReceipts { + pub fingerprints_with_receipts: Vec<(TransactionReceiptResult, PendingPayableFingerprint)>, + pub response_skeleton_opt: Option, +} + #[derive(Debug, Message, PartialEq)] pub struct SentPayables { pub payment_procedure_result: Result, PayableTransactionError>, @@ -150,6 +156,12 @@ pub struct ScanForPayables { pub response_skeleton_opt: Option, } +impl SettableSkeletonOptHolder for ScanForPayables { + fn set_skeleton(&mut self, response_skeleton: ResponseSkeleton) { + todo!() + } +} + #[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] pub struct ScanForReceivables { pub response_skeleton_opt: Option, @@ -193,8 +205,7 @@ impl Handler for Accountant { &self.logger, "Started with --scans on; starting database and blockchain scans" ); - - ctx.notify(ScanForPayables { + ctx.notify(ScanForPendingPayables { response_skeleton_opt: None, }); ctx.notify(ScanForReceivables { @@ -204,17 +215,46 @@ impl Handler for Accountant { } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ReceivedPayments, _ctx: &mut Self::Context) -> Self::Result { - if let Some(node_to_ui_msg) = self.scanners.receivable.finish_scan(msg, &self.logger) { - self.ui_message_sub_opt - .as_ref() - .expect("UIGateway is not bound") - .try_send(node_to_ui_msg) - .expect("UIGateway is dead"); + fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { + let response_skeleton = msg.response_skeleton_opt; + if self.handle_request_of_scan_for_pending_payable(response_skeleton) { + self.schedule_next_scan(ScanType::Payables, ctx, response_skeleton) } + self.schedule_next_scan(ScanType::PendingPayables, ctx, None) + } +} + +impl Handler for Accountant { + type Result = (); + + fn handle(&mut self, msg: ScanForPayables, _ctx: &mut Self::Context) -> Self::Result { + let response_skeleton = msg.response_skeleton_opt; + self.handle_request_of_scan_for_payable(response_skeleton); + } +} + +impl Handler for Accountant { + type Result = (); + + fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { + self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); + self.schedule_next_scan(ScanType::Receivables, ctx, None); + } +} + + +impl Handler for Accountant { + type Result = (); + + fn handle(&mut self, msg: ReportTransactionReceipts, ctx: &mut Self::Context) -> Self::Result { + let response_skeleton_opt = msg.response_skeleton_opt; + + let _ = self.scanners.pending_payable.finish_scan(msg, &self.logger); + + self.schedule_next_scan(ScanType::Payables, ctx, response_skeleton_opt) } } @@ -244,30 +284,17 @@ impl Handler for Accountant { } } -impl Handler for Accountant { - type Result = (); - - fn handle(&mut self, msg: ScanForPendingPayables, _ctx: &mut Self::Context) -> Self::Result { - let response_skeleton = msg.response_skeleton_opt; - self.handle_request_of_scan_for_pending_payable(response_skeleton); - } -} - -impl Handler for Accountant { - type Result = (); - - fn handle(&mut self, msg: ScanForPayables, _ctx: &mut Self::Context) -> Self::Result { - let response_skeleton = msg.response_skeleton_opt; - self.handle_request_of_scan_for_payable(response_skeleton); - } -} - -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { - self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); - self.schedule_next_scan(ScanType::Receivables, ctx); + fn handle(&mut self, msg: ReceivedPayments, _ctx: &mut Self::Context) -> Self::Result { + if let Some(node_to_ui_msg) = self.scanners.receivable.finish_scan(msg, &self.logger) { + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + } } } @@ -353,6 +380,10 @@ pub trait SkeletonOptHolder { fn skeleton_opt(&self) -> Option; } +pub trait SettableSkeletonOptHolder { + fn set_skeleton(&mut self, response_skeleton: ResponseSkeleton); +} + #[derive(Debug, PartialEq, Eq, Message, Clone)] pub struct RequestTransactionReceipts { pub pending_payable: Vec, @@ -365,24 +396,6 @@ impl SkeletonOptHolder for RequestTransactionReceipts { } } -#[derive(Debug, PartialEq, Eq, Message, Clone)] -pub struct ReportTransactionReceipts { - pub fingerprints_with_receipts: Vec<(TransactionReceiptResult, PendingPayableFingerprint)>, - pub response_skeleton_opt: Option, -} - -impl Handler for Accountant { - type Result = (); - - fn handle(&mut self, msg: ReportTransactionReceipts, _ctx: &mut Self::Context) -> Self::Result { - let response_skeleton_opt = msg.response_skeleton_opt; - - let _ = self.scanners.pending_payable.finish_scan(msg, &self.logger); - - self.handle_request_of_scan_for_payable(response_skeleton_opt) - } -} - impl Handler for Accountant { type Result = (); fn handle( @@ -596,12 +609,12 @@ impl Accountant { } } - fn schedule_next_scan(&self, scan_type: ScanType, ctx: &mut Context) { + fn schedule_next_scan(&self, scan_type: ScanType, ctx: &mut Context, response_skeleton_opt: Option) { self.scan_schedulers .schedulers .get(&scan_type) .unwrap_or_else(|| panic!("Scan Scheduler {:?} not properly prepared", scan_type)) - .schedule(ctx) + .schedule(ctx, response_skeleton_opt) } fn handle_report_routing_service_provided_message( @@ -869,9 +882,7 @@ impl Accountant { fn handle_request_of_scan_for_pending_payable( &mut self, response_skeleton_opt: Option, - ) { - // let result: Result = self.scanners.start_scan_guarded(); - + ) -> bool { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_scan_guarded( @@ -884,22 +895,28 @@ impl Accountant { }; match result { - Ok(scan_message) => self - .request_transaction_receipts_sub_opt - .as_ref() - .expect("BlockchainBridge is unbound") - .try_send(scan_message) - .expect("BlockchainBridge is dead"), + Ok(scan_message) => { + self + .request_transaction_receipts_sub_opt + .as_ref() + .expect("BlockchainBridge is unbound") + .try_send(scan_message) + .expect("BlockchainBridge is dead"); + todo!() //false + } Err(e) => { - if e == BeginScanError::NothingToProcess { - todo!() - } - e.handle_error( &self.logger, ScanType::PendingPayables, response_skeleton_opt.is_some(), - ) + ); + + //TODO rewrite into e==BeginScanError::NothingToProcess after testing + if e == BeginScanError::NothingToProcess { + true + } else { + todo!() + } } } } @@ -939,7 +956,9 @@ impl Accountant { ) { match scan_type { CommendableScanType::Payables => { - self.handle_request_of_scan_for_pending_payable(Some(response_skeleton)) + if self.handle_request_of_scan_for_pending_payable(Some(response_skeleton)) { + todo!() + } } CommendableScanType::Receivables => { self.handle_request_of_scan_for_receivable(Some(response_skeleton)) @@ -2170,8 +2189,8 @@ mod tests { fn periodical_scanning_for_payable_works() { init_test_logging(); let test_name = "periodical_scanning_for_payable_works"; - let begin_pending_payable_scan_params_arc = Arc::new(Mutex::new(vec![])); - let begin_payable_scan_params_arc = Arc::new(Mutex::new(vec![])); + let start_scan_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); + let start_scan_payable_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); let notify_payable_params_arc = Arc::new(Mutex::new(vec![])); let system = System::new(test_name); @@ -2179,7 +2198,6 @@ mod tests { let blockchain_bridge = blockchain_bridge.system_stop_conditions(match_every_type_id!( RequestTransactionReceipts, RequestTransactionReceipts, - QualifiedPayablesMessage, QualifiedPayablesMessage )); let blockchain_bridge_addr = blockchain_bridge.start(); @@ -2198,22 +2216,25 @@ mod tests { let pending_payable_scanner = ScannerMock::new() .started_at_result(None) .started_at_result(None) - .start_scan_params(&begin_pending_payable_scan_params_arc) + .started_at_result(None) + .start_scan_params(&start_scan_pending_payable_params_arc) .start_scan_result(Err(BeginScanError::NothingToProcess)) - .start_scan_result(Ok(request_transaction_receipts_1)) - .start_scan_result(Ok(request_transaction_receipts_2)); + .start_scan_result(Ok(request_transaction_receipts_1.clone())) + .start_scan_result(Ok(request_transaction_receipts_2.clone())); + let qualified_payables_msg = QualifiedPayablesMessage { + protected_qualified_payables: protect_payables_in_test(vec![make_payable_account( + 123, + )]), + consuming_wallet: consuming_wallet.clone(), + response_skeleton_opt: None, + }; let payable_scanner = ScannerMock::new() .started_at_result(None) .started_at_result(None) - .start_scan_params(&begin_payable_scan_params_arc) + .started_at_result(None) + .start_scan_params(&start_scan_payable_params_arc) .start_scan_result(Err(BeginScanError::NothingToProcess)) - .start_scan_result(Ok(QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(vec![make_payable_account( - 123, - )]), - consuming_wallet: consuming_wallet.clone(), - response_skeleton_opt: None, - })); + .start_scan_result(Ok(qualified_payables_msg.clone())); let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(97), @@ -2270,7 +2291,7 @@ mod tests { &consuming_wallet, time_before, time_after, - &begin_pending_payable_scan_params_arc, + &start_scan_pending_payable_params_arc, ); let (payable_first_attempt_timestamp, payable_second_attempt_timestamp) = assert_start_scan_params_after_called_twice( @@ -2280,7 +2301,7 @@ mod tests { &consuming_wallet, time_before, time_after, - &begin_payable_scan_params_arc, + &start_scan_payable_params_arc, ); assert!(pending_payable_first_attempt_timestamp < payable_first_attempt_timestamp); assert!(pending_payable_second_attempt_timestamp < payable_second_attempt_timestamp); @@ -2325,7 +2346,21 @@ mod tests { ] ); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); - blockchain_bridge_recording.get_record() + let actual_requested_receipts_1 = blockchain_bridge_recording.get_record::(0); + assert_eq!( + actual_requested_receipts_1, + &request_transaction_receipts_1 + ); + let actual_requested_receipts_2 = blockchain_bridge_recording.get_record::(1); + assert_eq!( + actual_requested_receipts_2, + &request_transaction_receipts_2 + ); + let actual_qualified_payables = blockchain_bridge_recording.get_record::(2); + assert_eq!( + actual_qualified_payables, + &qualified_payables_msg + ) } fn assert_start_scan_params_after_called_twice( diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 965a5e0a4..c298c7851 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -19,7 +19,7 @@ use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ }; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport}; use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; -use crate::accountant::{PendingPayableId, ScanForPendingPayables}; +use crate::accountant::{PendingPayableId, ScanForPendingPayables, SettableSkeletonOptHolder}; use crate::accountant::{ comma_joined_stringifiable, gwei_to_wei, Accountant, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForPayables, @@ -131,6 +131,7 @@ impl GuardedStartableScanner for Scanners { response_skeleton_opt: Option, logger: &Logger, ) -> Result { + eprintln!("Trying to start the pending payable scan"); match ( self.pending_payable.scan_started_at(), self.payable.scan_started_at(), @@ -1214,7 +1215,7 @@ impl ScanSchedulers { } pub trait ScanScheduler { - fn schedule(&self, ctx: &mut Context); + fn schedule(&self, ctx: &mut Context, response_skeleton_opt: Option); as_any_ref_in_trait!(); as_any_mut_in_trait!(); } @@ -1225,7 +1226,7 @@ pub struct PeriodicalScanScheduler { } impl ScanScheduler for PeriodicalScanScheduler { - fn schedule(&self, ctx: &mut Context) { + fn schedule(&self, ctx: &mut Context, _response_skeleton_opt: Option) { // the default of the message implies response_skeleton_opt to be None // because scheduled scans don't respond let _ = self @@ -1240,10 +1241,13 @@ pub struct ImminentScanScheduler { pub handle: Box>, } -impl ScanScheduler for ImminentScanScheduler { - fn schedule(&self, ctx: &mut Context) { - // the default of the message implies response_skeleton_opt to be None - // because scheduled scans don't respond +impl ScanScheduler for ImminentScanScheduler { + fn schedule(&self, ctx: &mut Context, response_skeleton_opt: Option) { + let mut msg = Message::default(); + if let Some(skeleton) = response_skeleton_opt { + todo!() + //msg.set_skeleton(skeleton) + } let _ = self.handle.notify(Message::default(), ctx); } as_any_ref_in_trait_impl!(); From 06c9135a0d5c6c3b70f4212c73e60b574e3bb382 Mon Sep 17 00:00:00 2001 From: Bert Date: Fri, 11 Apr 2025 21:39:24 +0200 Subject: [PATCH 03/49] GH-602: four scans layout installed --- .../tests/verify_bill_payment.rs | 6 +- node/src/accountant/mod.rs | 96 +-- node/src/accountant/payment_adjuster.rs | 8 +- .../scanners/mid_scan_msg_handling/mod.rs | 3 - node/src/accountant/scanners/mod.rs | 660 ++++++++++++------ .../payable_scanner/agent_null.rs | 6 +- .../payable_scanner/agent_web3.rs | 6 +- .../payable_scanner/blockchain_agent.rs | 0 .../payable_scanner/mod.rs | 22 +- .../payable_scanner/msgs.rs | 6 +- .../payable_scanner/test_utils.rs | 2 +- node/src/accountant/test_utils.rs | 12 +- node/src/blockchain/blockchain_bridge.rs | 8 +- .../blockchain_interface_web3/mod.rs | 4 +- .../blockchain_interface_web3/utils.rs | 6 +- .../blockchain/blockchain_interface/mod.rs | 2 +- node/src/sub_lib/accountant.rs | 2 +- node/src/sub_lib/blockchain_bridge.rs | 4 +- node/src/test_utils/recorder.rs | 10 +- .../test_utils/recorder_stop_conditions.rs | 16 +- 20 files changed, 572 insertions(+), 307 deletions(-) delete mode 100644 node/src/accountant/scanners/mid_scan_msg_handling/mod.rs rename node/src/accountant/scanners/{mid_scan_msg_handling => }/payable_scanner/agent_null.rs (94%) rename node/src/accountant/scanners/{mid_scan_msg_handling => }/payable_scanner/agent_web3.rs (93%) rename node/src/accountant/scanners/{mid_scan_msg_handling => }/payable_scanner/blockchain_agent.rs (100%) rename node/src/accountant/scanners/{mid_scan_msg_handling => }/payable_scanner/mod.rs (60%) rename node/src/accountant/scanners/{mid_scan_msg_handling => }/payable_scanner/msgs.rs (86%) rename node/src/accountant/scanners/{mid_scan_msg_handling => }/payable_scanner/test_utils.rs (96%) diff --git a/multinode_integration_tests/tests/verify_bill_payment.rs b/multinode_integration_tests/tests/verify_bill_payment.rs index 5d682fea4..5d35bf98d 100644 --- a/multinode_integration_tests/tests/verify_bill_payment.rs +++ b/multinode_integration_tests/tests/verify_bill_payment.rs @@ -3,7 +3,7 @@ use bip39::{Language, Mnemonic, Seed}; use futures::Future; use masq_lib::blockchains::chains::Chain; use masq_lib::constants::WEIS_IN_GWEI; -use masq_lib::messages::{ScanType, ToMessageBody, UiScanRequest}; +use masq_lib::messages::{CommendableScanType, ToMessageBody, UiScanRequest}; use masq_lib::test_utils::utils::UrlHolder; use masq_lib::utils::{derivation_path, find_free_port, NeighborhoodModeLight}; use multinode_integration_tests_lib::blockchain::BlockchainServer; @@ -394,7 +394,7 @@ fn verify_pending_payables() { let ui_client = real_consuming_node.make_ui(ui_port); ui_client.send_request( UiScanRequest { - scan_type: ScanType::Payables, + scan_type: CommendableScanType::Payables, } .tmb(0), ); @@ -432,7 +432,7 @@ fn verify_pending_payables() { ); ui_client.send_request( UiScanRequest { - scan_type: ScanType::PendingPayables, + scan_type: CommendableScanType::Payables, } .tmb(0), ); diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 04214c148..91fc5f9e9 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -22,10 +22,10 @@ use crate::accountant::db_access_objects::utils::{ use crate::accountant::financials::visibility_restricted_module::{ check_query_is_within_tech_limits, financials_entry_check, }; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{ +use crate::accountant::scanners::payable_scanner::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; -use crate::accountant::scanners::{BeginScanError, ScanSchedulers, ScanType, Scanners, GuardedStartableScanner}; +use crate::accountant::scanners::{BeginScanError, ScanSchedulers, ScanType, Scanners, ScanWithStarter}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, PendingPayableFingerprintSeeds, RetrieveTransactions}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; @@ -76,6 +76,7 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; +use crate::accountant::scanners::payable_scanner::MultistageDualPayableScanner; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; pub const CRASH_KEY: &str = "ACCOUNTANT"; @@ -152,11 +153,16 @@ pub struct ScanForPendingPayables { } #[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] -pub struct ScanForPayables { +pub struct ScanForNewPayables { pub response_skeleton_opt: Option, } -impl SettableSkeletonOptHolder for ScanForPayables { +#[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] +pub struct ScanForRetryPayables { + pub response_skeleton_opt: Option, +} + +impl SettableSkeletonOptHolder for ScanForNewPayables { fn set_skeleton(&mut self, response_skeleton: ResponseSkeleton) { todo!() } @@ -227,10 +233,10 @@ impl Handler for Accountant { } } -impl Handler for Accountant { +impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ScanForPayables, _ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: ScanForNewPayables, _ctx: &mut Self::Context) -> Self::Result { let response_skeleton = msg.response_skeleton_opt; self.handle_request_of_scan_for_payable(response_skeleton); } @@ -245,7 +251,6 @@ impl Handler for Accountant { } } - impl Handler for Accountant { type Result = (); @@ -609,7 +614,12 @@ impl Accountant { } } - fn schedule_next_scan(&self, scan_type: ScanType, ctx: &mut Context, response_skeleton_opt: Option) { + fn schedule_next_scan( + &self, + scan_type: ScanType, + ctx: &mut Context, + response_skeleton_opt: Option, + ) { self.scan_schedulers .schedulers .get(&scan_type) @@ -854,7 +864,7 @@ impl Accountant { ) { let result: Result = match self.consuming_wallet_opt.as_ref() { - Some(consuming_wallet) => self.scanners.start_scan_guarded( + Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( consuming_wallet, SystemTime::now(), response_skeleton_opt, @@ -885,7 +895,7 @@ impl Accountant { ) -> bool { let result: Result = match self.consuming_wallet_opt.as_ref() { - Some(consuming_wallet) => self.scanners.start_scan_guarded( + Some(consuming_wallet) => self.scanners.start_pending_payable_scan_guarded( consuming_wallet, // This argument is not used and is therefore irrelevant SystemTime::now(), response_skeleton_opt, @@ -896,8 +906,7 @@ impl Accountant { match result { Ok(scan_message) => { - self - .request_transaction_receipts_sub_opt + self.request_transaction_receipts_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) @@ -926,7 +935,7 @@ impl Accountant { response_skeleton_opt: Option, ) { let result: Result = - self.scanners.start_scan_guarded( + self.scanners.start_receivable_scan_guarded( &self.earning_wallet, SystemTime::now(), response_skeleton_opt, @@ -1072,7 +1081,7 @@ mod tests { use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; use crate::accountant::payment_adjuster::Adjustment; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; + use crate::accountant::scanners::payable_scanner::test_utils::BlockchainAgentMock; use crate::accountant::scanners::test_utils::protect_payables_in_test; use crate::accountant::scanners::{BeginScanError, PeriodicalScanScheduler}; use crate::accountant::test_utils::DaoWithDestination::{ @@ -2211,7 +2220,7 @@ mod tests { }; let request_transaction_receipts_2 = RequestTransactionReceipts { pending_payable: vec![pending_payable_fingerprint_2], - response_skeleton_opt: None + response_skeleton_opt: None, }; let pending_payable_scanner = ScannerMock::new() .started_at_result(None) @@ -2222,9 +2231,7 @@ mod tests { .start_scan_result(Ok(request_transaction_receipts_1.clone())) .start_scan_result(Ok(request_transaction_receipts_2.clone())); let qualified_payables_msg = QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(vec![make_payable_account( - 123, - )]), + protected_qualified_payables: protect_payables_in_test(vec![make_payable_account(123)]), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: None, }; @@ -2260,7 +2267,7 @@ mod tests { subject.scan_schedulers.update_imminent_scheduler( ScanType::Payables, Some(Box::new( - NotifyHandleMock::::default() + NotifyHandleMock::::default() .notify_params(¬ify_payable_params_arc) .capture_msg_and_let_it_fly_on(), )), @@ -2334,33 +2341,27 @@ mod tests { assert_eq!( *notify_payables_params, vec![ - ScanForPayables { + ScanForNewPayables { response_skeleton_opt: None }, - ScanForPayables { + ScanForNewPayables { response_skeleton_opt: None }, - ScanForPayables { + ScanForNewPayables { response_skeleton_opt: None } ] ); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); - let actual_requested_receipts_1 = blockchain_bridge_recording.get_record::(0); - assert_eq!( - actual_requested_receipts_1, - &request_transaction_receipts_1 - ); - let actual_requested_receipts_2 = blockchain_bridge_recording.get_record::(1); - assert_eq!( - actual_requested_receipts_2, - &request_transaction_receipts_2 - ); - let actual_qualified_payables = blockchain_bridge_recording.get_record::(2); - assert_eq!( - actual_qualified_payables, - &qualified_payables_msg - ) + let actual_requested_receipts_1 = + blockchain_bridge_recording.get_record::(0); + assert_eq!(actual_requested_receipts_1, &request_transaction_receipts_1); + let actual_requested_receipts_2 = + blockchain_bridge_recording.get_record::(1); + assert_eq!(actual_requested_receipts_2, &request_transaction_receipts_2); + let actual_qualified_payables = + blockchain_bridge_recording.get_record::(2); + assert_eq!(actual_qualified_payables, &qualified_payables_msg) } fn assert_start_scan_params_after_called_twice( @@ -2537,9 +2538,12 @@ mod tests { .build(); subject.outbound_payments_instructions_sub_opt = Some(outbound_payments_instructions_sub); - let _result: Result = subject - .scanners - .start_scan_guarded(&consuming_wallet, SystemTime::now(), None, &subject.logger); + let _result = subject.scanners.start_new_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &subject.logger, + ); System::current().stop(); system.run(); @@ -2645,13 +2649,13 @@ mod tests { .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .bootstrapper_config(config) .build(); - let message_before = ScanForPayables { + let message_before = ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 111, context_id: 222, }), }; - let message_after = ScanForPayables { + let message_after = ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 333, context_id: 444, @@ -2661,12 +2665,12 @@ mod tests { let addr = subject.start(); addr.try_send(message_before.clone()).unwrap(); - addr.try_send(ScanForPayables { + addr.try_send(ScanForNewPayables { response_skeleton_opt: None, }) .unwrap(); - // We ignored the second ScanForPayables message because the first message meant a scan + // We ignored the second ScanForNewPayables message because the first message meant a scan // was already in progress; now let's reset the state by ending the first scan by a failure // and see that the third scan attempt will be processed willingly again. addr.try_send(ReportTransactionReceipts { @@ -2740,7 +2744,7 @@ mod tests { let account_addr = subject.start(); let _ = account_addr - .try_send(ScanForPayables { + .try_send(ScanForNewPayables { response_skeleton_opt: None, }) .unwrap(); @@ -3700,7 +3704,7 @@ mod tests { vec![vec![fingerprint_2_fourth_round.clone()]] ); let expected_scan_for_payable_msg_and_interval = ( - ScanForPayables { + ScanForNewPayables { response_skeleton_opt: None, }, Duration::from_millis(payable_scan_interval), diff --git a/node/src/accountant/payment_adjuster.rs b/node/src/accountant/payment_adjuster.rs index 88ee13e74..519019e18 100644 --- a/node/src/accountant/payment_adjuster.rs +++ b/node/src/accountant/payment_adjuster.rs @@ -1,7 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::PreparedAdjustment; +use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner::PreparedAdjustment; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use masq_lib::logger::Logger; use std::time::SystemTime; @@ -71,8 +71,8 @@ pub enum AnalysisError {} #[cfg(test)] mod tests { use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; + use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; + use crate::accountant::scanners::payable_scanner::test_utils::BlockchainAgentMock; use crate::accountant::scanners::test_utils::protect_payables_in_test; use crate::accountant::test_utils::make_payable_account; use masq_lib::logger::Logger; diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/mod.rs b/node/src/accountant/scanners/mid_scan_msg_handling/mod.rs deleted file mode 100644 index 16331e4bf..000000000 --- a/node/src/accountant/scanners/mid_scan_msg_handling/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -pub mod payable_scanner; diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index c298c7851..49425ca64 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -pub mod mid_scan_msg_handling; +pub mod payable_scanner; pub mod scanners_utils; pub mod test_utils; @@ -19,10 +19,10 @@ use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ }; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport}; use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; -use crate::accountant::{PendingPayableId, ScanForPendingPayables, SettableSkeletonOptHolder}; +use crate::accountant::{PendingPayableId, ScanForPendingPayables, ScanForRetryPayables, SettableSkeletonOptHolder}; use crate::accountant::{ comma_joined_stringifiable, gwei_to_wei, Accountant, ReceivedPayments, - ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForPayables, + ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, ScanForReceivables, SentPayables, }; use crate::accountant::db_access_objects::banned_dao::BannedDao; @@ -44,14 +44,15 @@ use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; +use std::marker::PhantomData; use std::rc::Rc; use std::time::{Duration, SystemTime}; use time::format_description::parse; use time::OffsetDateTime; use web3::types::H256; use masq_lib::type_obfuscation::Obfuscated; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::{PreparedAdjustment, MultistagePayableScanner, SolvencySensitivePaymentInstructor}; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; +use crate::accountant::scanners::payable_scanner::{PreparedAdjustment, MultistageDualPayableScanner, SolvencySensitivePaymentInstructor}; +use crate::accountant::scanners::payable_scanner::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::db_config::persistent_configuration::{PersistentConfiguration, PersistentConfigurationReal}; @@ -64,10 +65,17 @@ pub enum ScanType { } pub struct Scanners { - pub payable: Box>, - pub pending_payable: - Box>, - pub receivable: Box>, + pub payable: Box>, + pub pending_payable: Box< + dyn StartableAccessibleScanner< + ScanForPendingPayables, + RequestTransactionReceipts, + ReportTransactionReceipts, + >, + >, + pub receivable: Box< + dyn StartableAccessibleScanner, + >, } impl Scanners { @@ -94,6 +102,7 @@ impl Scanners { let persistent_configuration = PersistentConfigurationReal::from(dao_factories.config_dao_factory.make()); + let receivable = Box::new(ReceivableScanner::new( dao_factories.receivable_dao_factory.make(), dao_factories.banned_dao_factory.make(), @@ -108,30 +117,14 @@ impl Scanners { receivable, } } -} - -pub trait GuardedStartableScanner -where - StartMessage: Message, -{ - fn start_scan_guarded( - &mut self, - wallet: &Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, - ) -> Result; -} -impl GuardedStartableScanner for Scanners { - fn start_scan_guarded( + pub fn start_pending_payable_scan_guarded( &mut self, wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, ) -> Result { - eprintln!("Trying to start the pending payable scan"); match ( self.pending_payable.scan_started_at(), self.payable.scan_started_at(), @@ -163,10 +156,30 @@ impl GuardedStartableScanner for Scanners { logger, ) } -} -impl GuardedStartableScanner for Scanners { - fn start_scan_guarded( + pub fn start_new_payable_scan_guarded( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + if let Some(started_at) = self.payable.scan_started_at() { + return Err(BeginScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Payables, + started_at, + }); + } + + let starter: PrivateScanStarter = + self.payable.scan_starter(); + + starter + .scanner + .start_scan(wallet, timestamp, response_skeleton_opt, logger) + } + + pub fn start_retry_payable_scan_guarded( &mut self, wallet: &Wallet, timestamp: SystemTime, @@ -175,23 +188,22 @@ impl GuardedStartableScanner for Scanners { ) -> Result { if let Some(started_at) = self.payable.scan_started_at() { unreachable!( - "Guard for pending payables should've prevented running the tandem of scanners if \ - the payable scanner was still running. It started {} and is still running at {}", + "Guard for pending payables should've prevented running the tandem of scanners \ + if the payable scanner was still running. It started {} and is still running at {}", BeginScanError::timestamp_as_string(started_at), BeginScanError::timestamp_as_string(SystemTime::now()) ) } - self.payable.scan_starter().scanner.start_scan( - wallet, - timestamp, - response_skeleton_opt, - logger, - ) + + let starter: PrivateScanStarter = + self.payable.scan_starter(); + + starter + .scanner + .start_scan(wallet, timestamp, response_skeleton_opt, logger) } -} -impl GuardedStartableScanner for Scanners { - fn start_scan_guarded( + pub fn start_receivable_scan_guarded( &mut self, wallet: &Wallet, timestamp: SystemTime, @@ -213,12 +225,25 @@ impl GuardedStartableScanner for Scanners { } } +pub trait StartableAccessibleScanner: + ScanWithStarter + AccessibleScanner +where + TriggerMessage: Message, + StartMessage: Message, + EndMessage: Message, +{ +} + +pub trait ScanWithStarter { + fn scan_starter(&mut self) -> PrivateScanStarter; +} + pub trait AccessibleScanner where StartMessage: Message, EndMessage: Message, { - fn scan_starter(&mut self) -> PrivateScanStarter; + // fn scan_starter(starter: &mut dyn InaccessibleScanner) -> PrivateScanStarter where Self: Sized; fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> Option; fn scan_started_at(&self) -> Option; fn mark_as_started(&mut self, timestamp: SystemTime); @@ -230,22 +255,27 @@ where // Using this Access token to screen away a private interface from its public counterpart, because // Rust doesn't allow selective private methods within a public trait -pub struct PrivateScanStarter<'s, StartMessage> { +pub struct PrivateScanStarter<'s, TriggerMessage, StartMessage> { + phantom: PhantomData, // Strictly private - scanner: &'s mut dyn InaccessibleScanner, + scanner: &'s mut dyn InaccessibleScanner, } -impl<'s, StartMessage> PrivateScanStarter<'s, StartMessage> { +impl<'s, TriggerMessage, StartMessage> PrivateScanStarter<'s, TriggerMessage, StartMessage> { fn new( - scanner: &'s mut dyn InaccessibleScanner, - ) -> PrivateScanStarter<'s, StartMessage> { - Self { scanner } + scanner: &'s mut dyn InaccessibleScanner, + ) -> PrivateScanStarter<'s, TriggerMessage, StartMessage> { + Self { + phantom: PhantomData::default(), + scanner, + } } } // Strictly private -trait InaccessibleScanner +trait InaccessibleScanner where + TriggerMessage: Message, StartMessage: Message, { fn start_scan( @@ -293,6 +323,7 @@ impl ScannerCommon { } } +#[macro_export] macro_rules! time_marking_methods { ($scan_type_variant: ident) => { fn scan_started_at(&self) -> Option { @@ -314,14 +345,16 @@ macro_rules! time_marking_methods { } pub struct PayableScanner { + pub payable_threshold_gauge: Box, pub common: ScannerCommon, pub payable_dao: Box, pub pending_payable_dao: Box, - pub payable_threshold_gauge: Box, pub payment_adjuster: Box, } -impl InaccessibleScanner for PayableScanner { +impl MultistageDualPayableScanner for PayableScanner {} + +impl InaccessibleScanner for PayableScanner { fn start_scan( &mut self, consuming_wallet: &Wallet, @@ -330,7 +363,7 @@ impl InaccessibleScanner for PayableScanner { logger: &Logger, ) -> Result { self.mark_as_started(timestamp); - info!(logger, "Scanning for payables"); + info!(logger, "Scanning for new payables"); let all_non_pending_payables = self.payable_dao.non_pending_payables(); debug!( @@ -365,11 +398,33 @@ impl InaccessibleScanner for PayableScanner { } } -impl AccessibleScanner for PayableScanner { - fn scan_starter(&mut self) -> PrivateScanStarter { +impl InaccessibleScanner for PayableScanner { + fn start_scan( + &mut self, + consuming_wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + todo!() + } +} + +impl ScanWithStarter for PayableScanner { + fn scan_starter(&mut self) -> PrivateScanStarter { + PrivateScanStarter::new(self) + } +} + +impl ScanWithStarter for PayableScanner { + fn scan_starter( + &mut self, + ) -> PrivateScanStarter { PrivateScanStarter::new(self) } +} +impl AccessibleScanner for PayableScanner { fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> Option { let (sent_payables, err_opt) = separate_errors(&message, logger); debug!( @@ -431,8 +486,6 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { } } -impl MultistagePayableScanner for PayableScanner {} - impl PayableScanner { pub fn new( payable_dao: Box, @@ -535,7 +588,7 @@ impl PayableScanner { } let sent_payables_hashes = hashes.iter().copied().collect::>(); - if !PayableScanner::is_symmetrical(sent_payables_hashes, hashes_from_db) { + if !Self::is_symmetrical(sent_payables_hashes, hashes_from_db) { panic!( "Inconsistency in two maps, they cannot be matched by hashes. Data set directly \ sent from BlockchainBridge: {:?}, set derived from the DB: {:?}", @@ -684,11 +737,11 @@ impl PayableScanner { }; } - fn protect_payables(&self, payables: Vec) -> Obfuscated { + pub(super) fn protect_payables(&self, payables: Vec) -> Obfuscated { Obfuscated::obfuscate_vector(payables) } - fn expose_payables(&self, obfuscated: Obfuscated) -> Vec { + pub(super) fn expose_payables(&self, obfuscated: Obfuscated) -> Vec { obfuscated.expose_vector() } } @@ -701,7 +754,26 @@ pub struct PendingPayableScanner { pub financial_statistics: Rc>, } -impl InaccessibleScanner for PendingPayableScanner { +impl + StartableAccessibleScanner< + ScanForPendingPayables, + RequestTransactionReceipts, + ReportTransactionReceipts, + > for PendingPayableScanner +{ +} + +impl ScanWithStarter for PendingPayableScanner { + fn scan_starter( + &mut self, + ) -> PrivateScanStarter { + PrivateScanStarter::new(self) + } +} + +impl InaccessibleScanner + for PendingPayableScanner +{ fn start_scan( &mut self, _wallet: &Wallet, @@ -735,10 +807,6 @@ impl InaccessibleScanner for PendingPayableScanner { impl AccessibleScanner for PendingPayableScanner { - fn scan_starter(&mut self) -> PrivateScanStarter { - PrivateScanStarter::new(self) - } - fn finish_scan( &mut self, message: ReportTransactionReceipts, @@ -934,7 +1002,18 @@ pub struct ReceivableScanner { pub financial_statistics: Rc>, } -impl InaccessibleScanner for ReceivableScanner { +impl StartableAccessibleScanner + for ReceivableScanner +{ +} + +impl ScanWithStarter for ReceivableScanner { + fn scan_starter(&mut self) -> PrivateScanStarter { + PrivateScanStarter::new(self) + } +} + +impl InaccessibleScanner for ReceivableScanner { fn start_scan( &mut self, earning_wallet: &Wallet, @@ -974,10 +1053,6 @@ impl AccessibleScanner for ReceivableSca // }) // } - fn scan_starter(&mut self) -> PrivateScanStarter { - PrivateScanStarter::new(self) - } - fn finish_scan(&mut self, msg: ReceivedPayments, logger: &Logger) -> Option { self.handle_new_received_payments(&msg, logger); self.mark_as_ended(logger); @@ -1198,10 +1273,10 @@ impl ScanSchedulers { ), ( ScanType::Payables, - Box::new(ImminentScanScheduler::{ - handle: Box::new(NotifyHandleReal::default()) - }) - ), + Box::new(ImminentScanScheduler:: { + handle: Box::new(NotifyHandleReal::default()), + }), + ), ( ScanType::Receivables, Box::new(PeriodicalScanScheduler:: { @@ -1215,7 +1290,11 @@ impl ScanSchedulers { } pub trait ScanScheduler { - fn schedule(&self, ctx: &mut Context, response_skeleton_opt: Option); + fn schedule( + &self, + ctx: &mut Context, + response_skeleton_opt: Option, + ); as_any_ref_in_trait!(); as_any_mut_in_trait!(); } @@ -1226,7 +1305,11 @@ pub struct PeriodicalScanScheduler { } impl ScanScheduler for PeriodicalScanScheduler { - fn schedule(&self, ctx: &mut Context, _response_skeleton_opt: Option) { + fn schedule( + &self, + ctx: &mut Context, + _response_skeleton_opt: Option, + ) { // the default of the message implies response_skeleton_opt to be None // because scheduled scans don't respond let _ = self @@ -1241,8 +1324,14 @@ pub struct ImminentScanScheduler { pub handle: Box>, } -impl ScanScheduler for ImminentScanScheduler { - fn schedule(&self, ctx: &mut Context, response_skeleton_opt: Option) { +impl ScanScheduler + for ImminentScanScheduler +{ + fn schedule( + &self, + ctx: &mut Context, + response_skeleton_opt: Option, + ) { let mut msg = Message::default(); if let Some(skeleton) = response_skeleton_opt { todo!() @@ -1256,15 +1345,17 @@ impl ScanScheduler for I #[cfg(test)] pub mod local_test_utils { - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; + use crate::accountant::scanners::payable_scanner::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::{ AccessibleScanner, BeginScanError, ImminentScanScheduler, InaccessibleScanner, - MultistagePayableScanner, PeriodicalScanScheduler, PreparedAdjustment, PrivateScanStarter, - ScanSchedulers, ScanType, SolvencySensitivePaymentInstructor, + MultistageDualPayableScanner, PayableScanner, PeriodicalScanScheduler, PreparedAdjustment, + PrivateScanStarter, ScanSchedulers, ScanType, ScanWithStarter, + SolvencySensitivePaymentInstructor, StartableAccessibleScanner, }; - use crate::accountant::BlockchainAgentWithContextMessage; use crate::accountant::OutboundPaymentsInstructions; use crate::accountant::{Accountant, ResponseSkeleton, SentPayables}; + use crate::accountant::{BlockchainAgentWithContextMessage, ScanForNewPayables}; + use crate::sub_lib::peer_actors::StartMessage; use crate::sub_lib::utils::{NotifyHandle, NotifyLaterHandle}; use crate::sub_lib::wallet::Wallet; use actix::{Message, System}; @@ -1278,7 +1369,7 @@ pub mod local_test_utils { macro_rules! formal_traits_for_payable_mid_scan_msg_handling { ($scanner:ty) => { - impl MultistagePayableScanner for $scanner {} + impl MultistageDualPayableScanner for $scanner {} impl SolvencySensitivePaymentInstructor for $scanner { fn try_skipping_payment_adjustment( @@ -1302,15 +1393,30 @@ pub mod local_test_utils { pub struct NullScanner {} - impl AccessibleScanner for NullScanner + impl + StartableAccessibleScanner for NullScanner where + TriggerMessage: Message, StartMessage: Message, EndMessage: Message, { - fn scan_starter(&mut self) -> PrivateScanStarter { - PrivateScanStarter::new(self) + } + + impl ScanWithStarter for NullScanner + where + TriggerMessage: Message, + StartMessage: Message, + { + fn scan_starter(&mut self) -> PrivateScanStarter { + todo!() } + } + impl AccessibleScanner for NullScanner + where + StartMessage: Message, + EndMessage: Message, + { fn finish_scan( &mut self, _message: EndMessage, @@ -1336,8 +1442,9 @@ pub mod local_test_utils { formal_traits_for_payable_mid_scan_msg_handling!(NullScanner); - impl InaccessibleScanner for NullScanner + impl InaccessibleScanner for NullScanner where + TriggerMessage: Message, StartMessage: Message, { fn start_scan( @@ -1372,9 +1479,32 @@ pub mod local_test_utils { stop_system_after_last_message: RefCell, } - impl InaccessibleScanner + impl + StartableAccessibleScanner for ScannerMock where + TriggerMessage: Message, + StartMessage: Message, + EndMessage: Message, + { + } + + impl ScanWithStarter + for ScannerMock + where + TriggerMessage: Message, + StartMessage: Message, + EndMessage: Message, + { + fn scan_starter(&mut self) -> PrivateScanStarter { + todo!() + } + } + + impl InaccessibleScanner + for ScannerMock + where + TriggerMessage: Message, StartMessage: Message, EndMessage: Message, { @@ -1404,10 +1534,6 @@ pub mod local_test_utils { StartMessage: Message, EndMessage: Message, { - fn scan_starter(&mut self) -> PrivateScanStarter { - PrivateScanStarter::new(self) - } - fn finish_scan( &mut self, message: EndMessage, @@ -1541,13 +1667,13 @@ mod tests { PendingPayable, PendingPayableDaoError, TransactionHashes, }; use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t}; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; + use crate::accountant::scanners::payable_scanner::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PendingPayableMetadata; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; use crate::accountant::scanners::test_utils::protect_payables_in_test; - use crate::accountant::scanners::{BeginScanError, PayableScanner, PendingPayableScanner, ReceivableScanner, ScanSchedulers, ScanType, AccessibleScanner, ScannerCommon, Scanners, InaccessibleScanner, GuardedStartableScanner, PeriodicalScanScheduler, ImminentScanScheduler}; + use crate::accountant::scanners::{BeginScanError, PendingPayableScanner, ReceivableScanner, ScanSchedulers, ScanType, AccessibleScanner, ScannerCommon, Scanners, InaccessibleScanner, PeriodicalScanScheduler, ImminentScanScheduler, PayableScanner, ScanWithStarter, PrivateScanStarter}; use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; - use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanForPayables, ScanForPendingPayables, ScanForReceivables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; + use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanForNewPayables, ScanForPendingPayables, ScanForReceivables, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ @@ -1695,9 +1821,9 @@ mod tests { } #[test] - fn payable_scanner_can_initiate_a_scan() { + fn new_payable_scanner_can_initiate_a_scan() { init_test_logging(); - let test_name = "payable_scanner_can_initiate_a_scan"; + let test_name = "new_payable_scanner_can_initiate_a_scan"; let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); let (qualified_payable_accounts, _, all_non_pending_payables) = @@ -1710,8 +1836,12 @@ mod tests { .build(); subject.payable = Box::new(payable_scanner); - let result: Result = - subject.start_scan_guarded(&consuming_wallet, now, None, &Logger::new(test_name)); + let result = subject.start_new_payable_scan_guarded( + &consuming_wallet, + now, + None, + &Logger::new(test_name), + ); let timestamp = subject.payable.scan_started_at(); assert_eq!(timestamp, Some(now)); @@ -1726,7 +1856,7 @@ mod tests { }) ); TestLogHandler::new().assert_logs_match_in_order(vec![ - &format!("INFO: {test_name}: Scanning for payables"), + &format!("INFO: {test_name}: Scanning for new payables"), &format!( "INFO: {test_name}: Chose {} qualified debts to pay", qualified_payable_accounts.len() @@ -1735,7 +1865,111 @@ mod tests { } #[test] - fn payable_scanner_panics_if_requested_when_already_running() { + fn new_payable_scanner_cannot_be_initiated_if_it_is_already_running() { + let consuming_wallet = make_paying_wallet(b"consuming wallet"); + let (_, _, all_non_pending_payables) = + make_payables(SystemTime::now(), &PaymentThresholds::default()); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + let mut subject = make_dull_subject(); + let payable_scanner = PayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(); + subject.payable = Box::new(payable_scanner); + let previous_scan_started_at = SystemTime::now(); + let _ = subject.start_new_payable_scan_guarded( + &consuming_wallet, + previous_scan_started_at, + None, + &Logger::new("test"), + ); + + let result = subject.start_new_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + ); + + let is_scan_running = subject.payable.scan_started_at().is_some(); + assert_eq!(is_scan_running, true); + assert_eq!( + result, + Err(BeginScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Payables, + started_at: previous_scan_started_at + }) + ); + } + + #[test] + fn new_payable_scanner_throws_error_in_case_no_qualified_payable_is_found() { + let consuming_wallet = make_paying_wallet(b"consuming wallet"); + let now = SystemTime::now(); + let (_, unqualified_payable_accounts, _) = + make_payables(now, &PaymentThresholds::default()); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(unqualified_payable_accounts); + let mut subject = PayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(); + let starter: PrivateScanStarter = subject.scan_starter(); + + let result = starter + .scanner + .start_scan(&consuming_wallet, now, None, &Logger::new("test")); + + let is_scan_running = subject.scan_started_at().is_some(); + assert_eq!(is_scan_running, false); + assert_eq!(result, Err(BeginScanError::NothingToProcess)); + } + + #[test] + fn retry_payable_scanner_can_initiate_a_scan() { + init_test_logging(); + let test_name = "retry_payable_scanner_can_initiate_a_scan"; + let consuming_wallet = make_paying_wallet(b"consuming wallet"); + let now = SystemTime::now(); + let (qualified_payable_accounts, _, all_non_pending_payables) = + make_payables(now, &PaymentThresholds::default()); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + let mut subject = make_dull_subject(); + let payable_scanner = PayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(); + subject.payable = Box::new(payable_scanner); + + let result = subject.start_retry_payable_scan_guarded( + &consuming_wallet, + now, + None, + &Logger::new(test_name), + ); + + let timestamp = subject.payable.scan_started_at(); + assert_eq!(timestamp, Some(now)); + assert_eq!( + result, + Ok(QualifiedPayablesMessage { + protected_qualified_payables: protect_payables_in_test( + qualified_payable_accounts.clone() + ), + consuming_wallet, + response_skeleton_opt: None, + }) + ); + TestLogHandler::new().assert_logs_match_in_order(vec![ + &format!("INFO: {test_name}: Scanning for retry-required payables"), + &format!( + "INFO: {test_name}: Chose {} qualified debts to pay", + qualified_payable_accounts.len() + ), + ]) + } + + #[test] + fn retry_payable_scanner_panics_in_case_scan_is_already_running() { let consuming_wallet = make_paying_wallet(b"consuming wallet"); let (_, _, all_non_pending_payables) = make_payables(SystemTime::now(), &PaymentThresholds::default()); @@ -1747,7 +1981,7 @@ mod tests { .build(); subject.payable = Box::new(payable_scanner); let before = SystemTime::now(); - let _: Result = subject.start_scan_guarded( + let _ = subject.start_retry_payable_scan_guarded( &consuming_wallet, SystemTime::now(), None, @@ -1755,12 +1989,13 @@ mod tests { ); let caught_panic = catch_unwind(AssertUnwindSafe(|| { - let _: Result = subject.start_scan_guarded( - &consuming_wallet, - SystemTime::now(), - None, - &Logger::new("test"), - ); + let _: Result = subject + .start_retry_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + ); })) .unwrap_err(); @@ -1782,86 +2017,39 @@ mod tests { expected_needle_2, panic_msg ); + check_timestamps_in_panic_for_already_running_retry_payable_scanner( + &panic_msg, before, after, + ) + } + + fn check_timestamps_in_panic_for_already_running_retry_payable_scanner( + panic_msg: &str, + before: SystemTime, + after: SystemTime, + ) { let regex = PseudoTimestamp::regex(); let mut captures = regex.captures_iter(panic_msg); - let first_actual_as_pseudo_timestamp = PseudoTimestamp::new_from_captures(&mut captures); - let second_actual_as_pseudo_timestamp = PseudoTimestamp::new_from_captures(&mut captures); - let before_as_pseudo_timestamp = PseudoTimestamp::from(before); - let after_as_pseudo_timestamp = PseudoTimestamp::from(after); + let first_actual_as_pseudo_t = PseudoTimestamp::new_from_captures(&mut captures); + let second_actual_as_pseudo_t = PseudoTimestamp::new_from_captures(&mut captures); + let before_as_pseudo_t = PseudoTimestamp::from(before); + let after_as_pseudo_t = PseudoTimestamp::from(after); + assert!( - before_as_pseudo_timestamp <= first_actual_as_pseudo_timestamp - && first_actual_as_pseudo_timestamp <= second_actual_as_pseudo_timestamp - && second_actual_as_pseudo_timestamp <= after_as_pseudo_timestamp, + before_as_pseudo_t <= first_actual_as_pseudo_t + && first_actual_as_pseudo_t <= second_actual_as_pseudo_t + && second_actual_as_pseudo_t <= after_as_pseudo_t, "We expected this relationship before({:?}) <= first_actual({:?}) <= second_actual({:?}) \ <= after({:?}), but it does not hold true", - before_as_pseudo_timestamp, - first_actual_as_pseudo_timestamp, - second_actual_as_pseudo_timestamp, - after_as_pseudo_timestamp + before_as_pseudo_t, + first_actual_as_pseudo_t, + second_actual_as_pseudo_t, + after_as_pseudo_t ); } - // Concatenated hours, minutes, seconds and milliseconds in a single integer - #[derive(PartialEq, Debug)] - struct PseudoTimestamp { - rep: u32, - } - - // This is a on-hour difference, indicating wrapping around the midnight - const MIDNIGHT_INDICATIVE_DIFFERENCE_DIFF: u32 = 1_000_000; - - impl PartialOrd for PseudoTimestamp { - fn partial_cmp(&self, other: &Self) -> Option { - if self.rep == other.rep { - Some(Ordering::Equal) - } else if self.rep < other.rep { - if (other.rep - self.rep) > MIDNIGHT_INDICATIVE_DIFFERENCE_DIFF { - Some(Ordering::Greater) - } else { - Some(Ordering::Less) - } - } else { - if (self.rep - other.rep) > MIDNIGHT_INDICATIVE_DIFFERENCE_DIFF { - Some(Ordering::Less) - } else { - Some(Ordering::Greater) - } - } - } - } - - impl From for PseudoTimestamp { - fn from(timestamp: SystemTime) -> Self { - let specially_formatted_timestamp = BeginScanError::timestamp_as_string(timestamp); - let regex = Self::regex(); - let mut captures = regex.captures_iter(&specially_formatted_timestamp); - PseudoTimestamp::new_from_captures(&mut captures) - } - } - - impl PseudoTimestamp { - fn new_from_captures(captures: &mut CaptureMatches) -> Self { - let captured_first_time = captures.next().unwrap().get(1).unwrap().as_str(); - let num = Self::remove_colons_and_dots(captured_first_time); - Self { - rep: u32::from_str_radix(&num, 10).unwrap(), - } - } - - fn regex() -> Regex { - Regex::new(r"\d{4}-\d{2}-\d{2} (\d{2}:\d{2}:\d{2}\.\d{3})").unwrap() - } - - fn remove_colons_and_dots(str: &str) -> String { - let mut str = str.to_string(); - str = str.replace(":", ""); - str = str.replace(".", ""); - str - } - } - #[test] - fn payable_scanner_throws_error_in_case_no_qualified_payable_is_found() { + #[should_panic(expected = "bluh")] + fn retry_payable_scanner_panics_in_case_no_qualified_payable_is_found() { let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); let (_, unqualified_payable_accounts, _) = @@ -1871,17 +2059,11 @@ mod tests { let mut subject = PayableScannerBuilder::new() .payable_dao(payable_dao) .build(); + let starter: PrivateScanStarter = subject.scan_starter(); - let result = subject.scan_starter().scanner.start_scan( - &consuming_wallet, - now, - None, - &Logger::new("test"), - ); - - let is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(is_scan_running, false); - assert_eq!(result, Err(BeginScanError::NothingToProcess)); + let _ = starter + .scanner + .start_scan(&consuming_wallet, now, None, &Logger::new("test")); } #[test] @@ -2788,8 +2970,12 @@ mod tests { let payable_scanner = PayableScannerBuilder::new().build(); subject.payable = Box::new(payable_scanner); - let result: Result = - subject.start_scan_guarded(&consuming_wallet, now, None, &Logger::new(test_name)); + let result = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + now, + None, + &Logger::new(test_name), + ); let no_of_pending_payables = fingerprints.len(); let is_scan_running = subject.pending_payable.scan_started_at().is_some(); @@ -2823,11 +3009,14 @@ mod tests { let payable_scanner = PayableScannerBuilder::new().build(); subject.payable = Box::new(payable_scanner); let logger = Logger::new("test"); - let _: Result = - subject.start_scan_guarded(&consuming_wallet, now, None, &logger); + let _ = subject.start_pending_payable_scan_guarded(&consuming_wallet, now, None, &logger); - let result: Result = - subject.start_scan_guarded(&consuming_wallet, SystemTime::now(), None, &logger); + let result = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &logger, + ); let is_scan_running = subject.pending_payable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); @@ -2852,8 +3041,12 @@ mod tests { let previous_scan_started_at = SystemTime::now(); subject.payable.mark_as_started(previous_scan_started_at); - let result: Result = - subject.start_scan_guarded(&consuming_wallet, SystemTime::now(), None, &logger); + let result = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &logger, + ); let is_scan_running = subject.pending_payable.scan_started_at().is_some(); assert_eq!(is_scan_running, false); @@ -2886,7 +3079,7 @@ mod tests { .mark_as_started(timestamp_payable_scanner_start); let caught_panic = catch_unwind(AssertUnwindSafe(|| { - let _: Result = subject.start_scan_guarded( + let _ = subject.start_pending_payable_scan_guarded( &consuming_wallet, SystemTime::now(), None, @@ -3574,8 +3767,12 @@ mod tests { .build(); subject.receivable = Box::new(receivable_scanner); - let result: Result = - subject.start_scan_guarded(&earning_wallet, now, None, &Logger::new(test_name)); + let result = subject.start_receivable_scan_guarded( + &earning_wallet, + now, + None, + &Logger::new(test_name), + ); let is_scan_running = subject.receivable.scan_started_at().is_some(); assert_eq!(is_scan_running, true); @@ -3603,10 +3800,10 @@ mod tests { .receivable_dao(receivable_dao) .build(); subject.receivable = Box::new(receivable_scanner); - let _: Result = - subject.start_scan_guarded(&earning_wallet, now, None, &Logger::new("test")); + let _ = + subject.start_receivable_scan_guarded(&earning_wallet, now, None, &Logger::new("test")); - let result: Result = subject.start_scan_guarded( + let result = subject.start_receivable_scan_guarded( &earning_wallet, SystemTime::now(), None, @@ -4090,7 +4287,7 @@ mod tests { .get(&ScanType::Payables) .unwrap() .as_any() - .downcast_ref::>() + .downcast_ref::>() .unwrap(); assert_eq!( result @@ -4112,4 +4309,63 @@ mod tests { receivable: Box::new(NullScanner::new()), } } + + // Concatenated hours, minutes, seconds and milliseconds in a single integer + #[derive(PartialEq, Debug)] + struct PseudoTimestamp { + rep: u32, + } + + // This is a one-hour difference, indicating wrapping around the midnight + const MIDNIGHT_INDICATIVE_DIFFERENCE_DIFF: u32 = 1_000_000; + + impl PartialOrd for PseudoTimestamp { + fn partial_cmp(&self, other: &Self) -> Option { + if self.rep == other.rep { + Some(Ordering::Equal) + } else if self.rep < other.rep { + if (other.rep - self.rep) > MIDNIGHT_INDICATIVE_DIFFERENCE_DIFF { + Some(Ordering::Greater) + } else { + Some(Ordering::Less) + } + } else { + if (self.rep - other.rep) > MIDNIGHT_INDICATIVE_DIFFERENCE_DIFF { + Some(Ordering::Less) + } else { + Some(Ordering::Greater) + } + } + } + } + + impl From for PseudoTimestamp { + fn from(timestamp: SystemTime) -> Self { + let specially_formatted_timestamp = BeginScanError::timestamp_as_string(timestamp); + let regex = Self::regex(); + let mut captures = regex.captures_iter(&specially_formatted_timestamp); + PseudoTimestamp::new_from_captures(&mut captures) + } + } + + impl PseudoTimestamp { + fn new_from_captures(captures: &mut CaptureMatches) -> Self { + let captured_first_time = captures.next().unwrap().get(1).unwrap().as_str(); + let num = Self::remove_colons_and_dots(captured_first_time); + Self { + rep: u32::from_str_radix(&num, 10).unwrap(), + } + } + + fn regex() -> Regex { + Regex::new(r"\d{4}-\d{2}-\d{2} (\d{2}:\d{2}:\d{2}\.\d{3})").unwrap() + } + + fn remove_colons_and_dots(str: &str) -> String { + let mut str = str.to_string(); + str = str.replace(":", ""); + str = str.replace(".", ""); + str + } + } } diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_null.rs b/node/src/accountant/scanners/payable_scanner/agent_null.rs similarity index 94% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_null.rs rename to node/src/accountant/scanners/payable_scanner/agent_null.rs index e95673002..9c70c3dc3 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_null.rs +++ b/node/src/accountant/scanners/payable_scanner/agent_null.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; @@ -77,8 +77,8 @@ impl Default for BlockchainAgentNull { #[cfg(test)] mod tests { - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_null::BlockchainAgentNull; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; + use crate::accountant::scanners::payable_scanner::agent_null::BlockchainAgentNull; + use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_web3.rs b/node/src/accountant/scanners/payable_scanner/agent_web3.rs similarity index 93% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_web3.rs rename to node/src/accountant/scanners/payable_scanner/agent_web3.rs index 725e14f00..de1a87aa5 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_web3.rs +++ b/node/src/accountant/scanners/payable_scanner/agent_web3.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use masq_lib::blockchains::chains::Chain; @@ -64,10 +64,10 @@ impl BlockchainAgentWeb3 { #[cfg(test)] mod tests { - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::{ + use crate::accountant::scanners::payable_scanner::agent_web3::{ BlockchainAgentWeb3, WEB3_MAXIMAL_GAS_LIMIT_MARGIN, }; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; + use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::test_utils::make_wallet; use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/blockchain_agent.rs b/node/src/accountant/scanners/payable_scanner/blockchain_agent.rs similarity index 100% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/blockchain_agent.rs rename to node/src/accountant/scanners/payable_scanner/blockchain_agent.rs diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner/mod.rs similarity index 60% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs rename to node/src/accountant/scanners/payable_scanner/mod.rs index 2fa2c0043..b2d3d61da 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/mod.rs @@ -6,16 +6,24 @@ pub mod blockchain_agent; pub mod msgs; pub mod test_utils; -use crate::accountant::payment_adjuster::Adjustment; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::AccessibleScanner; +use crate::accountant::db_access_objects::payable_dao::PayableDao; +use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDao; +use crate::accountant::payment_adjuster::{Adjustment, PaymentAdjuster}; +use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::{AccessibleScanner, InaccessibleScanner, ScanWithStarter}; +use crate::accountant::{ScanForNewPayables, ScanForRetryPayables}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use actix::Message; -use itertools::Either; +use itertools::{Either, Itertools}; use masq_lib::logger::Logger; +use masq_lib::messages::ToMessageBody; +use masq_lib::utils::ExpectValue; -pub trait MultistagePayableScanner: - AccessibleScanner + SolvencySensitivePaymentInstructor +pub trait MultistageDualPayableScanner: + ScanWithStarter + + ScanWithStarter + + AccessibleScanner + + SolvencySensitivePaymentInstructor where StartMessage: Message, EndMessage: Message, @@ -55,7 +63,7 @@ impl PreparedAdjustment { #[cfg(test)] mod tests { - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::PreparedAdjustment; + use crate::accountant::scanners::payable_scanner::PreparedAdjustment; impl Clone for PreparedAdjustment { fn clone(&self) -> Self { diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/msgs.rs b/node/src/accountant/scanners/payable_scanner/msgs.rs similarity index 86% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/msgs.rs rename to node/src/accountant/scanners/payable_scanner/msgs.rs index 41a1b3940..fcdfc4430 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/msgs.rs +++ b/node/src/accountant/scanners/payable_scanner/msgs.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; use crate::accountant::{ResponseSkeleton, SkeletonOptHolder}; use crate::sub_lib::wallet::Wallet; use actix::Message; @@ -58,8 +58,8 @@ impl BlockchainAgentWithContextMessage { #[cfg(test)] mod tests { - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; + use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; + use crate::accountant::scanners::payable_scanner::test_utils::BlockchainAgentMock; impl Clone for BlockchainAgentWithContextMessage { fn clone(&self) -> Self { diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs b/node/src/accountant/scanners/payable_scanner/test_utils.rs similarity index 96% rename from node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs rename to node/src/accountant/scanners/payable_scanner/test_utils.rs index d3ab97284..921266bbd 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs +++ b/node/src/accountant/scanners/payable_scanner/test_utils.rs @@ -2,7 +2,7 @@ #![cfg(test)] -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 0e928fa29..a68a1b7cd 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -14,8 +14,8 @@ use crate::accountant::db_access_objects::receivable_dao::{ }; use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdjuster}; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::PreparedAdjustment; +use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner::PreparedAdjustment; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; use crate::accountant::scanners::{PayableScanner, PendingPayableScanner, ReceivableScanner}; use crate::accountant::{gwei_to_wei, Accountant, DEFAULT_PENDING_TOO_LONG_SEC}; @@ -118,7 +118,7 @@ pub enum DaoWithDestination { enum DestinationMarker { AccountantBody, PendingPayableScanner, - PayableScanner, + SharedPayableScanner, ReceivableScanner, } @@ -130,7 +130,7 @@ impl DaoWithDestination { matches!(dest_marker, DestinationMarker::PendingPayableScanner) } Self::ForPayableScanner(_) => { - matches!(dest_marker, DestinationMarker::PayableScanner) + matches!(dest_marker, DestinationMarker::SharedPayableScanner) } Self::ForReceivableScanner(_) => { matches!(dest_marker, DestinationMarker::ReceivableScanner) @@ -235,13 +235,13 @@ macro_rules! create_or_update_factory { const PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::AccountantBody, - DestinationMarker::PayableScanner, + DestinationMarker::SharedPayableScanner, DestinationMarker::PendingPayableScanner, ]; const PENDING_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::AccountantBody, - DestinationMarker::PayableScanner, + DestinationMarker::SharedPayableScanner, DestinationMarker::PendingPayableScanner, ]; diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index ddcbd2778..0b940e271 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{ +use crate::accountant::scanners::payable_scanner::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; use crate::accountant::{ @@ -45,7 +45,7 @@ use std::time::SystemTime; use ethabi::Hash; use web3::types::H256; use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; use crate::accountant::scanners::ScanType; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; @@ -549,8 +549,8 @@ mod tests { use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; use crate::accountant::db_access_objects::utils::from_time_t; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; + use crate::accountant::scanners::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; + use crate::accountant::scanners::payable_scanner::test_utils::BlockchainAgentMock; use crate::accountant::scanners::test_utils::protect_payables_in_test; use crate::accountant::test_utils::{make_payable_account, make_pending_payable_fingerprint}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::BlockchainInterfaceWeb3; diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs index 92f8e9145..a0269c3ab 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -4,7 +4,7 @@ pub mod lower_level_interface_web3; mod utils; use std::cmp::PartialEq; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; @@ -430,7 +430,7 @@ impl BlockchainInterfaceWeb3 { #[cfg(test)] mod tests { use super::*; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; + use crate::accountant::scanners::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_LITERAL, TRANSFER_METHOD_ID, diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs index 2be7d5977..f8cc16d71 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -2,8 +2,8 @@ use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::BlockchainAgentWeb3; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner::agent_web3::BlockchainAgentWeb3; +use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, HashAndAmount, TRANSFER_METHOD_ID, @@ -332,7 +332,7 @@ mod tests { use super::*; use crate::accountant::db_access_objects::utils::from_time_t; use crate::accountant::gwei_to_wei; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; + use crate::accountant::scanners::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::accountant::test_utils::{ make_payable_account, make_payable_account_with_wallet_and_balance_and_timestamp_opt, }; diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index ef1e8d373..7bd5e5ca8 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -6,7 +6,7 @@ pub mod lower_level_interface; use actix::Recipient; use ethereum_types::H256; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainAgentBuildError, BlockchainError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RetrievedBlockchainTransactions}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index a8cc1cdba..f4b893db9 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -3,7 +3,7 @@ use crate::accountant::db_access_objects::banned_dao::BannedDaoFactory; use crate::accountant::db_access_objects::payable_dao::PayableDaoFactory; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDaoFactory; use crate::accountant::db_access_objects::receivable_dao::ReceivableDaoFactory; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; use crate::accountant::{ checked_conversion, Accountant, ReceivedPayments, ReportTransactionReceipts, ScanError, SentPayables, diff --git a/node/src/sub_lib/blockchain_bridge.rs b/node/src/sub_lib/blockchain_bridge.rs index 2ec840cc9..a6f3f2364 100644 --- a/node/src/sub_lib/blockchain_bridge.rs +++ b/node/src/sub_lib/blockchain_bridge.rs @@ -1,8 +1,8 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; +use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner::msgs::QualifiedPayablesMessage; use crate::accountant::{RequestTransactionReceipts, ResponseSkeleton, SkeletonOptHolder}; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::sub_lib::peer_actors::BindMessage; diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index 7a8b0acc0..3e10a4d70 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -1,12 +1,12 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. #![cfg(test)] -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; +use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner::msgs::QualifiedPayablesMessage; use crate::accountant::ReportTransactionReceipts; use crate::accountant::{ - ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForPayables, ScanForReceivables, - SentPayables, + ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForNewPayables, + ScanForReceivables, SentPayables, }; use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; use crate::blockchain::blockchain_bridge::RetrieveTransactions; @@ -162,7 +162,7 @@ recorder_message_handler_t_m_p!(ReportTransactionReceipts); recorder_message_handler_t_m_p!(RequestTransactionReceipts); recorder_message_handler_t_m_p!(RetrieveTransactions); recorder_message_handler_t_m_p!(ScanError); -recorder_message_handler_t_m_p!(ScanForPayables); +recorder_message_handler_t_m_p!(ScanForNewPayables); recorder_message_handler_t_m_p!(ScanForReceivables); recorder_message_handler_t_m_p!(SentPayables); recorder_message_handler_t_m_p!(StartMessage); diff --git a/node/src/test_utils/recorder_stop_conditions.rs b/node/src/test_utils/recorder_stop_conditions.rs index 9e3c92212..88c9b757a 100644 --- a/node/src/test_utils/recorder_stop_conditions.rs +++ b/node/src/test_utils/recorder_stop_conditions.rs @@ -156,7 +156,7 @@ macro_rules! match_every_type_id{ mod tests { use crate::accountant::scanners::ScanType; - use crate::accountant::{ResponseSkeleton, ScanError, ScanForPayables}; + use crate::accountant::{ResponseSkeleton, ScanError, ScanForNewPayables}; use crate::daemon::crash_notification::CrashNotification; use crate::sub_lib::peer_actors::{NewPublicIp, StartMessage}; use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; @@ -168,7 +168,7 @@ mod tests { fn remove_matched_conditions_works_with_unsorted_indexes() { let mut conditions = vec![ StopCondition::StopOnType(TypeId::of::()), - StopCondition::StopOnType(TypeId::of::()), + StopCondition::StopOnType(TypeId::of::()), StopCondition::StopOnType(TypeId::of::()), ]; let indexes = vec![2, 0]; @@ -181,7 +181,7 @@ mod tests { } else { panic!("expected StopOnType but got a different variant") }; - assert_eq!(type_id, TypeId::of::()) + assert_eq!(type_id, TypeId::of::()) } #[test] @@ -254,7 +254,7 @@ mod tests { exemplar: Box::new(StartMessage {}), }, ]); - let first_msg = ScanForPayables { + let first_msg = ScanForNewPayables { response_skeleton_opt: None, }; let second_msg = StartMessage {}; @@ -269,7 +269,7 @@ mod tests { }; assert_eq!( - cond_set.resolve_stop_conditions::(&first_msg), + cond_set.resolve_stop_conditions::(&first_msg), false ); let len_after_stage_1 = inspect_len_of_any(&cond_set, 1); @@ -301,7 +301,7 @@ mod tests { }), }, StopCondition::StopOnMatch { - exemplar: Box::new(ScanForPayables { + exemplar: Box::new(ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 789, @@ -310,14 +310,14 @@ mod tests { }, StopCondition::StopOnType(TypeId::of::()), ]); - let tested_msg_1 = ScanForPayables { + let tested_msg_1 = ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 789, }), }; - let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_1); + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_1); assert_eq!(kill_system, false); match &cond_set { From ad0ec6ad91d55e1427187730db3cb9a7d35f8071 Mon Sep 17 00:00:00 2001 From: Bert Date: Fri, 18 Apr 2025 00:02:55 +0200 Subject: [PATCH 04/49] GH-602: mostly done but still some crumbs of work left --- node/src/accountant/mod.rs | 1464 +++++++++-------- node/src/accountant/scanners/mod.rs | 386 ++--- .../scanners/payable_scanner/mod.rs | 10 +- .../accountant/scanners/scan_schedulers.rs | 303 ++++ .../src/accountant/scanners/scanners_utils.rs | 68 +- node/src/accountant/scanners/test_utils.rs | 41 + node/src/accountant/test_utils.rs | 2 +- .../src/db_config/persistent_configuration.rs | 3 +- node/src/node_configurator/configurator.rs | 2 + .../unprivileged_parse_args_configuration.rs | 16 +- node/src/sub_lib/accountant.rs | 3 + node/src/sub_lib/combined_parameters.rs | 11 +- node/src/test_utils/mod.rs | 33 + 13 files changed, 1343 insertions(+), 999 deletions(-) create mode 100644 node/src/accountant/scanners/scan_schedulers.rs diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 91fc5f9e9..152f82e58 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -25,7 +25,7 @@ use crate::accountant::financials::visibility_restricted_module::{ use crate::accountant::scanners::payable_scanner::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; -use crate::accountant::scanners::{BeginScanError, ScanSchedulers, ScanType, Scanners, ScanWithStarter}; +use crate::accountant::scanners::{BeginScanError, ScanType, Scanners, UiScanResult}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, PendingPayableFingerprintSeeds, RetrieveTransactions}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; @@ -76,7 +76,7 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; -use crate::accountant::scanners::payable_scanner::MultistageDualPayableScanner; +use crate::accountant::scanners::scan_schedulers::ScanSchedulers; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; pub const CRASH_KEY: &str = "ACCOUNTANT"; @@ -162,12 +162,6 @@ pub struct ScanForRetryPayables { pub response_skeleton_opt: Option, } -impl SettableSkeletonOptHolder for ScanForNewPayables { - fn set_skeleton(&mut self, response_skeleton: ResponseSkeleton) { - todo!() - } -} - #[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] pub struct ScanForReceivables { pub response_skeleton_opt: Option, @@ -227,9 +221,8 @@ impl Handler for Accountant { fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { let response_skeleton = msg.response_skeleton_opt; if self.handle_request_of_scan_for_pending_payable(response_skeleton) { - self.schedule_next_scan(ScanType::Payables, ctx, response_skeleton) + self.scan_schedulers.payable.schedule_for_new_payable(ctx) } - self.schedule_next_scan(ScanType::PendingPayables, ctx, None) } } @@ -238,7 +231,16 @@ impl Handler for Accountant { fn handle(&mut self, msg: ScanForNewPayables, _ctx: &mut Self::Context) -> Self::Result { let response_skeleton = msg.response_skeleton_opt; - self.handle_request_of_scan_for_payable(response_skeleton); + self.handle_request_of_scan_for_new_payable(response_skeleton); + } +} + +impl Handler for Accountant { + type Result = (); + + fn handle(&mut self, msg: ScanForRetryPayables, _ctx: &mut Self::Context) -> Self::Result { + let response_skeleton = msg.response_skeleton_opt; + self.handle_request_of_scan_for_retry_payable(response_skeleton); } } @@ -247,7 +249,7 @@ impl Handler for Accountant { fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); - self.schedule_next_scan(ScanType::Receivables, ctx, None); + self.scan_schedulers.receivable.schedule(ctx, None); } } @@ -257,9 +259,24 @@ impl Handler for Accountant { fn handle(&mut self, msg: ReportTransactionReceipts, ctx: &mut Self::Context) -> Self::Result { let response_skeleton_opt = msg.response_skeleton_opt; - let _ = self.scanners.pending_payable.finish_scan(msg, &self.logger); - - self.schedule_next_scan(ScanType::Payables, ctx, response_skeleton_opt) + match self.scanners.pending_payable.finish_scan(msg, &self.logger) { + UiScanResult::Finished(ui_msg_opt) => { + if let Some(node_to_ui_msg) = ui_msg_opt { + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead") + } + self.scan_schedulers.payable.schedule_for_new_payable(ctx) + } + UiScanResult::FollowedByAnotherScan => { + todo!(); + self.scan_schedulers + .payable + .schedule_for_retry_payable(ctx, response_skeleton_opt) + } + }; } } @@ -278,14 +295,21 @@ impl Handler for Accountant { impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: SentPayables, _ctx: &mut Self::Context) -> Self::Result { - if let Some(node_to_ui_msg) = self.scanners.payable.finish_scan(msg, &self.logger) { + fn handle(&mut self, msg: SentPayables, ctx: &mut Self::Context) -> Self::Result { + if let Some(node_to_ui_msg) = self + .scanners + .payable + .finish_scan(msg, &self.logger) + .finished() + { self.ui_message_sub_opt .as_ref() .expect("UIGateway is not bound") .try_send(node_to_ui_msg) .expect("UIGateway is dead"); } + + self.scan_schedulers.pending_payable.schedule(ctx, None) } } @@ -293,7 +317,12 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ReceivedPayments, _ctx: &mut Self::Context) -> Self::Result { - if let Some(node_to_ui_msg) = self.scanners.receivable.finish_scan(msg, &self.logger) { + if let Some(node_to_ui_msg) = self + .scanners + .receivable + .finish_scan(msg, &self.logger) + .finished() + { self.ui_message_sub_opt .as_ref() .expect("UIGateway is not bound") @@ -385,10 +414,6 @@ pub trait SkeletonOptHolder { fn skeleton_opt(&self) -> Option; } -pub trait SettableSkeletonOptHolder { - fn set_skeleton(&mut self, response_skeleton: ResponseSkeleton); -} - #[derive(Debug, PartialEq, Eq, Message, Clone)] pub struct RequestTransactionReceipts { pub pending_payable: Vec, @@ -614,18 +639,18 @@ impl Accountant { } } - fn schedule_next_scan( - &self, - scan_type: ScanType, - ctx: &mut Context, - response_skeleton_opt: Option, - ) { - self.scan_schedulers - .schedulers - .get(&scan_type) - .unwrap_or_else(|| panic!("Scan Scheduler {:?} not properly prepared", scan_type)) - .schedule(ctx, response_skeleton_opt) - } + // fn schedule_next_scan( + // &self, + // scan_type: ScanType, + // ctx: &mut Context, + // response_skeleton_opt: Option, + // ) { + // self.scan_schedulers + // .schedulers + // .get(&scan_type) + // .unwrap_or_else(|| panic!("Scan Scheduler {:?} not properly prepared", scan_type)) + // .schedule(ctx, response_skeleton_opt) + // } fn handle_report_routing_service_provided_message( &mut self, @@ -858,7 +883,7 @@ impl Accountant { } } - fn handle_request_of_scan_for_payable( + fn handle_request_of_scan_for_new_payable( &mut self, response_skeleton_opt: Option, ) { @@ -889,6 +914,42 @@ impl Accountant { } } + fn handle_request_of_scan_for_retry_payable( + &mut self, + response_skeleton_opt: Option, + ) { + todo!( + "This must be written in a different card. The new start_scan method for \ + the payable scanner must be filled with code first - the SentPayableDao with its new \ + method allowing to filter out payments that need to be retried" + ) + // let result: Result = + // match self.consuming_wallet_opt.as_ref() { + // Some(consuming_wallet) => self.scanners.start_retry_payable_scan_guarded( + // consuming_wallet, + // SystemTime::now(), + // response_skeleton_opt, + // &self.logger, + // ), + // None => Err(BeginScanError::NoConsumingWalletFound), + // }; + // + // match result { + // Ok(scan_message) => { + // self.qualified_payables_sub_opt + // .as_ref() + // .expect("BlockchainBridge is unbound") + // .try_send(scan_message) + // .expect("BlockchainBridge is dead"); + // } + // Err(e) => e.handle_error( + // &self.logger, + // ScanType::Payables, + // response_skeleton_opt.is_some(), + // ), + // } + } + fn handle_request_of_scan_for_pending_payable( &mut self, response_skeleton_opt: Option, @@ -911,7 +972,8 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - todo!() //false + + false } Err(e) => { e.handle_error( @@ -920,11 +982,11 @@ impl Accountant { response_skeleton_opt.is_some(), ); - //TODO rewrite into e==BeginScanError::NothingToProcess after testing + //TODO rewrite into simple "e==BeginScanError::NothingToProcess" after testing if e == BeginScanError::NothingToProcess { true } else { - todo!() + false } } } @@ -1082,18 +1144,17 @@ mod tests { use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::protect_payables_in_test; - use crate::accountant::scanners::{BeginScanError, PeriodicalScanScheduler}; + use crate::accountant::scanners::test_utils::{protect_payables_in_test, NewPayableScanDynIntervalComputerMock}; + use crate::accountant::scanners::{BeginScanError}; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; - use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_payables, make_pending_payable_fingerprint, BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock}; + use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock}; use crate::accountant::test_utils::{AccountantBuilder, BannedDaoMock}; use crate::accountant::Accountant; - use crate::blockchain::blockchain_bridge::BlockchainBridge; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::test_utils::{ - make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, + make_tx_hash }; use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::database::test_utils::transaction_wrapper_mock::TransactionInnerWrapperMockBuilder; @@ -1107,7 +1168,6 @@ mod tests { use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::neighborhood::ConfigChange; use crate::sub_lib::neighborhood::{Hops, WalletPair}; - use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::recorder::make_recorder; use crate::test_utils::recorder::peer_actors_builder; use crate::test_utils::recorder::Recorder; @@ -1120,7 +1180,7 @@ mod tests { prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, }; use crate::test_utils::{make_paying_wallet, make_wallet}; - use actix::{Arbiter, System}; + use actix::{System}; use ethereum_types::U64; use ethsign_crypto::Keccak256; use log::Level; @@ -1135,19 +1195,17 @@ mod tests { }; use masq_lib::test_utils::logging::init_test_logging; use masq_lib::test_utils::logging::TestLogHandler; - use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use masq_lib::ui_gateway::MessagePath::Conversation; use masq_lib::ui_gateway::{MessageBody, MessagePath, NodeFromUiMessage, NodeToUiMessage}; - use masq_lib::utils::find_free_port; use std::any::TypeId; use std::ops::{Add, Sub}; - use std::str::FromStr; use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; use std::vec; use crate::accountant::scanners::local_test_utils::{NullScanner, ScannerMock}; + use crate::accountant::scanners::scan_schedulers::{NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; impl Handler> for Accountant { @@ -1262,14 +1320,16 @@ mod tests { let financial_statistics = result.financial_statistics().clone(); let default_scan_intervals = ScanIntervals::default(); - assert_scan_scheduler::( - &result, - ScanType::Payables, - default_scan_intervals.payable_scan_interval, + assert_eq!( + result.scan_schedulers.payable.nominal_interval, + default_scan_intervals.payable_scan_interval + ); + assert_eq!( + result.scan_schedulers.pending_payable.interval, + default_scan_intervals.pending_payable_scan_interval, ); - assert_scan_scheduler::( - &result, - ScanType::Receivables, + assert_eq!( + result.scan_schedulers.receivable.interval, default_scan_intervals.receivable_scan_interval, ); assert_eq!(result.consuming_wallet_opt, None); @@ -1285,25 +1345,6 @@ mod tests { assert_eq!(financial_statistics.total_paid_payable_wei, 0); } - fn assert_scan_scheduler( - accountant: &Accountant, - scan_type: ScanType, - expected_scan_interval: Duration, - ) { - assert_eq!( - accountant - .scan_schedulers - .schedulers - .get(&scan_type) - .unwrap() - .as_any() - .downcast_ref::>() - .unwrap() - .interval, - expected_scan_interval - ) - } - #[test] fn accountant_handles_config_change_msg() { assert_handling_of_config_change_msg( @@ -1371,6 +1412,7 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_millis(2_000), receivable_scan_interval: Duration::from_millis(10_000), }); let receivable_dao = ReceivableDaoMock::new() @@ -1417,6 +1459,7 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_millis(2_000), receivable_scan_interval: Duration::from_millis(10_000), }); config.suppress_initial_scans = true; @@ -1525,8 +1568,6 @@ mod tests { let subject_addr = subject.start(); let system = System::new("test"); let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - let sent_payable = SentPayables { payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct(PendingPayable { recipient_wallet: make_wallet("blah"), @@ -1537,6 +1578,7 @@ mod tests { context_id: 4321, }), }; + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); subject_addr.try_send(sent_payable).unwrap(); @@ -1815,102 +1857,68 @@ mod tests { // a response skeleton #[test] fn report_transaction_receipts_with_response_skeleton_sends_scan_response_to_ui_gateway() { - todo!("don't send the message yet...the payable scanner follows") - // let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); - // config.scan_intervals_opt = Some(ScanIntervals { - // payable_scan_interval: Duration::from_millis(10_000), - // receivable_scan_interval: Duration::from_millis(10_000), - // pending_payable_scan_interval: Duration::from_secs(100), - // }); - // let subject = AccountantBuilder::default() - // .bootstrapper_config(config) - // .build(); - // let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); - // let subject_addr = subject.start(); - // let system = System::new("test"); - // let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); - // subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - // let report_transaction_receipts = ReportTransactionReceipts { - // fingerprints_with_receipts: vec![], - // response_skeleton_opt: Some(ResponseSkeleton { - // client_id: 1234, - // context_id: 4321, - // }), - // }; - // - // subject_addr.try_send(report_transaction_receipts).unwrap(); - // - // System::current().stop(); - // system.run(); - // let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); - // assert_eq!( - // ui_gateway_recording.get_record::(0), - // &NodeToUiMessage { - // target: ClientId(1234), - // body: UiScanResponse {}.tmb(4321), - // } - // ); - } - - #[test] - fn accountant_calls_payable_dao_to_mark_pending_payable() { - let fingerprints_rowids_params_arc = Arc::new(Mutex::new(vec![])); - let mark_pending_payables_rowids_params_arc = Arc::new(Mutex::new(vec![])); - let expected_wallet = make_wallet("paying_you"); - let expected_hash = H256::from("transaction_hash".keccak256()); - let expected_rowid = 45623; - let pending_payable_dao = PendingPayableDaoMock::default() - .fingerprints_rowids_params(&fingerprints_rowids_params_arc) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![(expected_rowid, expected_hash)], - no_rowid_results: vec![], - }); - let payable_dao = PayableDaoMock::new() - .mark_pending_payables_rowids_params(&mark_pending_payables_rowids_params_arc) - .mark_pending_payables_rowids_result(Ok(())); - let system = System::new("accountant_calls_payable_dao_to_mark_pending_payable"); - let accountant = AccountantBuilder::default() - .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) - .payable_daos(vec![ForPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) + let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + receivable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_secs(100), + }); + let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); + let pending_payable = PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let subject = AccountantBuilder::default() + .bootstrapper_config(config) + .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable)]) .build(); - let expected_payable = PendingPayable::new(expected_wallet.clone(), expected_hash.clone()); - let sent_payable = SentPayables { - payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( - expected_payable.clone(), - )]), - response_skeleton_opt: None, + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let subject_addr = subject.start(); + let system = System::new("test"); + let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + let tx_receipt = TxReceipt { + transaction_hash: make_tx_hash(456), + status: TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(789), + block_number: 789012345.into(), + }), + }; + let pending_payable_fingerprint = make_pending_payable_fingerprint(); + let report_transaction_receipts = ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(tx_receipt), + pending_payable_fingerprint, + )], + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 1234, + context_id: 4321, + }), }; - let subject = accountant.start(); - subject - .try_send(sent_payable) - .expect("unexpected actix error"); + subject_addr.try_send(report_transaction_receipts).unwrap(); System::current().stop(); system.run(); - let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); - assert_eq!(*fingerprints_rowids_params, vec![vec![expected_hash]]); - let mark_pending_payables_rowids_params = - mark_pending_payables_rowids_params_arc.lock().unwrap(); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); assert_eq!( - *mark_pending_payables_rowids_params, - vec![vec![(expected_wallet, expected_rowid)]] + ui_gateway_recording.get_record::(0), + &NodeToUiMessage { + target: ClientId(1234), + body: UiScanResponse {}.tmb(4321), + } ); } #[test] - fn accountant_sends_initial_payable_payments_msg_when_qualified_payable_found() { + fn accountant_sends_qualified_payable_msg_when_qualified_payable_found() { let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let now = SystemTime::now(); let payment_thresholds = PaymentThresholds::default(); let (qualified_payables, _, all_non_pending_payables) = - make_payables(now, &payment_thresholds); + make_qualified_and_unqualified_payables(now, &payment_thresholds); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); - let system = System::new( - "accountant_sends_initial_payable_payments_msg_when_qualified_payable_found", - ); + let system = + System::new("accountant_sends_qualified_payable_msg_when_qualified_payable_found"); let consuming_wallet = make_paying_wallet(b"consuming"); let mut subject = AccountantBuilder::default() .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) @@ -1926,7 +1934,11 @@ mod tests { .build(); send_bind_message!(accountant_subs, peer_actors); - send_start_message!(accountant_subs); + accountant_addr + .try_send(ScanForNewPayables { + response_skeleton_opt: None, + }) + .unwrap(); System::current().stop(); system.run(); @@ -2060,6 +2072,9 @@ mod tests { let pending_payable_params_arc = Arc::new(Mutex::new(vec![])); let new_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); let paid_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); + let scan_for_new_payables_notify_params_arc = Arc::new(Mutex::new(vec![])); + let scan_for_new_payables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let scan_for_receivables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, _) = make_recorder(); let earning_wallet = make_wallet("earning"); let system = System::new("accountant_scans_after_startup"); @@ -2072,11 +2087,22 @@ mod tests { .new_delinquencies_result(vec![]) .paid_delinquencies_parameters(&paid_delinquencies_params_arc) .paid_delinquencies_result(vec![]); - let subject = AccountantBuilder::default() + let mut subject = AccountantBuilder::default() .bootstrapper_config(config) .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) .build(); + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&scan_for_new_payables_notify_later_params_arc), + ); + subject.scan_schedulers.payable.new_payable_notify = Box::new( + NotifyHandleMock::default().notify_params(&scan_for_new_payables_notify_params_arc), + ); + subject.scan_schedulers.receivable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&scan_for_receivables_notify_later_params_arc), + ); let peer_actors = peer_actors_builder() .blockchain_bridge(blockchain_bridge) .build(); @@ -2102,6 +2128,35 @@ mod tests { assert_eq!(captured_curves, PaymentThresholds::default()); assert_eq!(paid_delinquencies_params.len(), 1); assert_eq!(paid_delinquencies_params[0], PaymentThresholds::default()); + let scan_for_new_payables_notify_params = + scan_for_new_payables_notify_params_arc.lock().unwrap(); + let scan_for_receivables_notify_later_params = + scan_for_receivables_notify_later_params_arc.lock().unwrap(); + let default_scan_intervals = ScanIntervals::default(); + assert_eq!( + *scan_for_new_payables_notify_params, + vec![ScanForNewPayables { + response_skeleton_opt: None + }] + ); + let scan_for_new_payables_notify_later_params = + scan_for_new_payables_notify_later_params_arc + .lock() + .unwrap(); + assert!( + scan_for_new_payables_notify_later_params.is_empty(), + "Was meant to be empty but contained: {:?}", + scan_for_new_payables_notify_later_params + ); + assert_eq!( + *scan_for_receivables_notify_later_params, + vec![( + ScanForReceivables { + response_skeleton_opt: None + }, + default_scan_intervals.receivable_scan_interval + )] + ); let tlh = TestLogHandler::new(); tlh.exists_log_containing("INFO: Accountant: Scanning for pending payable"); tlh.exists_log_containing(&format!( @@ -2133,6 +2188,7 @@ mod tests { let mut config = bc_from_earning_wallet(earning_wallet.clone()); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_secs(100), + pending_payable_scan_interval: Duration::from_secs(10), receivable_scan_interval: Duration::from_millis(99), }); let mut subject = AccountantBuilder::default() @@ -2142,14 +2198,10 @@ mod tests { subject.scanners.payable = Box::new(NullScanner::new()); // Skipping subject.scanners.pending_payable = Box::new(NullScanner::new()); // Skipping subject.scanners.receivable = Box::new(receivable_scanner); - subject.scan_schedulers.update_periodical_scheduler( - ScanType::Receivables, - Some(Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(¬ify_later_receivable_params_arc) - .capture_msg_and_let_it_fly_on(), - )), - None, + subject.scan_schedulers.receivable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(¬ify_later_receivable_params_arc) + .capture_msg_and_let_it_fly_on(), ); let subject_addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); @@ -2194,174 +2246,179 @@ mod tests { ) } + // This test begins with the new payable scan, continues over the retry payable scan and ends + // with another attempt for new payables which proves one complete cycle. #[test] - fn periodical_scanning_for_payable_works() { - init_test_logging(); - let test_name = "periodical_scanning_for_payable_works"; - let start_scan_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); - let start_scan_payable_params_arc = Arc::new(Mutex::new(vec![])); - let notify_later_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); - let notify_payable_params_arc = Arc::new(Mutex::new(vec![])); - let system = System::new(test_name); - let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let blockchain_bridge = blockchain_bridge.system_stop_conditions(match_every_type_id!( - RequestTransactionReceipts, - RequestTransactionReceipts, - QualifiedPayablesMessage - )); - let blockchain_bridge_addr = blockchain_bridge.start(); - let consuming_wallet = make_paying_wallet(b"consuming"); - let pending_payable_fingerprint_1 = make_pending_payable_fingerprint(); - let mut pending_payable_fingerprint_2 = make_pending_payable_fingerprint(); - pending_payable_fingerprint_2.hash = make_tx_hash(987); - let request_transaction_receipts_1 = RequestTransactionReceipts { - pending_payable: vec![pending_payable_fingerprint_1], - response_skeleton_opt: None, - }; - let request_transaction_receipts_2 = RequestTransactionReceipts { - pending_payable: vec![pending_payable_fingerprint_2], - response_skeleton_opt: None, - }; - let pending_payable_scanner = ScannerMock::new() - .started_at_result(None) - .started_at_result(None) - .started_at_result(None) - .start_scan_params(&start_scan_pending_payable_params_arc) - .start_scan_result(Err(BeginScanError::NothingToProcess)) - .start_scan_result(Ok(request_transaction_receipts_1.clone())) - .start_scan_result(Ok(request_transaction_receipts_2.clone())); - let qualified_payables_msg = QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(vec![make_payable_account(123)]), - consuming_wallet: consuming_wallet.clone(), - response_skeleton_opt: None, - }; - let payable_scanner = ScannerMock::new() - .started_at_result(None) - .started_at_result(None) - .started_at_result(None) - .start_scan_params(&start_scan_payable_params_arc) - .start_scan_result(Err(BeginScanError::NothingToProcess)) - .start_scan_result(Ok(qualified_payables_msg.clone())); - let mut config = bc_from_earning_wallet(make_wallet("hi")); - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(97), - receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner - }); - let mut subject = AccountantBuilder::default() - .bootstrapper_config(config) - .consuming_wallet(consuming_wallet.clone()) - .logger(Logger::new(test_name)) - .build(); - subject.scanners.pending_payable = Box::new(pending_payable_scanner); - subject.scanners.payable = Box::new(payable_scanner); - subject.scanners.receivable = Box::new(NullScanner::new()); //skipping - subject.scan_schedulers.update_periodical_scheduler( - ScanType::PendingPayables, - Some(Box::new( - NotifyLaterHandleMock::::default() - .notify_later_params(¬ify_later_pending_payables_params_arc) - .capture_msg_and_let_it_fly_on(), - )), - None, - ); - subject.scan_schedulers.update_imminent_scheduler( - ScanType::Payables, - Some(Box::new( - NotifyHandleMock::::default() - .notify_params(¬ify_payable_params_arc) - .capture_msg_and_let_it_fly_on(), - )), - ); - subject.request_transaction_receipts_sub_opt = - Some(blockchain_bridge_addr.clone().recipient()); - subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); - let subject_addr = subject.start(); - let subject_subs = Accountant::make_subs_from(&subject_addr); - - send_start_message!(subject_subs); - - let time_before = SystemTime::now(); - system.run(); - let time_after = SystemTime::now(); - let tlh = TestLogHandler::new(); - tlh.assert_logs_contain_in_order(vec![ - &format!( - "DEBUG: {test_name}: There was nothing to process during PendingPayables scan." - ), - &format!("DEBUG: {test_name}: There was nothing to process during Payables scan."), - ]); - let (pending_payable_first_attempt_timestamp, pending_payable_second_attempt_timestamp) = - assert_start_scan_params_after_called_twice( - "pending payable", - test_name, - &tlh, - &consuming_wallet, - time_before, - time_after, - &start_scan_pending_payable_params_arc, - ); - let (payable_first_attempt_timestamp, payable_second_attempt_timestamp) = - assert_start_scan_params_after_called_twice( - "payable", - test_name, - &tlh, - &consuming_wallet, - time_before, - time_after, - &start_scan_payable_params_arc, - ); - assert!(pending_payable_first_attempt_timestamp < payable_first_attempt_timestamp); - assert!(pending_payable_second_attempt_timestamp < payable_second_attempt_timestamp); - let notify_later_pending_payables_params = - notify_later_pending_payables_params_arc.lock().unwrap(); - assert_eq!( - *notify_later_pending_payables_params, - vec![ - ( - ScanForPendingPayables { - response_skeleton_opt: None - }, - Duration::from_millis(97) - ), - ( - ScanForPendingPayables { - response_skeleton_opt: None - }, - Duration::from_millis(97) - ), - ( - ScanForPendingPayables { - response_skeleton_opt: None - }, - Duration::from_millis(97) - ), - ] - ); - let notify_payables_params = notify_payable_params_arc.lock().unwrap(); - assert_eq!( - *notify_payables_params, - vec![ - ScanForNewPayables { - response_skeleton_opt: None - }, - ScanForNewPayables { - response_skeleton_opt: None - }, - ScanForNewPayables { - response_skeleton_opt: None - } - ] - ); - let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); - let actual_requested_receipts_1 = - blockchain_bridge_recording.get_record::(0); - assert_eq!(actual_requested_receipts_1, &request_transaction_receipts_1); - let actual_requested_receipts_2 = - blockchain_bridge_recording.get_record::(1); - assert_eq!(actual_requested_receipts_2, &request_transaction_receipts_2); - let actual_qualified_payables = - blockchain_bridge_recording.get_record::(2); - assert_eq!(actual_qualified_payables, &qualified_payables_msg) + fn periodical_scanning_for_payables_works() { + todo!( + "in order to write up this test, I'd have to make the recorder able to send messages \ + in a reaction to other message arrival" + ) + // init_test_logging(); + // let test_name = "periodical_scanning_for_payable_works"; + // let start_scan_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); + // let start_scan_payable_params_arc = Arc::new(Mutex::new(vec![])); + // let notify_later_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); + // let notify_payable_params_arc = Arc::new(Mutex::new(vec![])); + // let system = System::new(test_name); + // let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + // let blockchain_bridge = blockchain_bridge.system_stop_conditions(match_every_type_id!( + // RequestTransactionReceipts, + // RequestTransactionReceipts, + // QualifiedPayablesMessage + // )); + // let blockchain_bridge_addr = blockchain_bridge.start(); + // let consuming_wallet = make_paying_wallet(b"consuming"); + // let mut pending_payable_fingerprint = make_pending_payable_fingerprint(); + // let request_transaction_receipts_1 = RequestTransactionReceipts { + // pending_payable: vec![pending_payable_fingerprint], + // response_skeleton_opt: None, + // }; + // let pending_payable_scanner = ScannerMock::new() + // .started_at_result(None) + // .started_at_result(None) + // .started_at_result(None) + // .start_scan_params(&start_scan_pending_payable_params_arc) + // .start_scan_result(Ok(request_transaction_receipts_1.clone())); + // let qualified_payables_msg = QualifiedPayablesMessage { + // protected_qualified_payables: protect_payables_in_test(vec![make_payable_account(123)]), + // consuming_wallet: consuming_wallet.clone(), + // response_skeleton_opt: None, + // }; + // let payable_scanner = ScannerMock::new() + // .started_at_result(None) + // .started_at_result(None) + // .started_at_result(None) + // .start_scan_params(&start_scan_payable_params_arc) + // .start_scan_result(Ok(qualified_payables_msg.clone())) + // .start_scan_result(Err(BeginScanError::NothingToProcess)); + // let mut config = bc_from_earning_wallet(make_wallet("hi")); + // config.scan_intervals_opt = Some(ScanIntervals { + // payable_scan_interval: Duration::from_millis(97), + // pending_payable_scan_interval: Duration::from_secs(100), // We'll never run this scanner + // receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner + // }); + // let pending_payable = PendingPayable::new(make_wallet("abc"), make_tx_hash(123)); + // let mut subject = AccountantBuilder::default() + // .bootstrapper_config(config) + // .consuming_wallet(consuming_wallet.clone()) + // .logger(Logger::new(test_name)) + // .build(); + // subject.scanners.pending_payable = Box::new(pending_payable_scanner); + // subject.scanners.payable = Box::new(payable_scanner); + // subject.scanners.receivable = Box::new(NullScanner::new()); //skipping + // subject.scan_schedulers.update_scheduler( + // ScanType::PendingPayables, + // Some(Box::new( + // NotifyLaterHandleMock::::default() + // .notify_later_params(¬ify_later_pending_payables_params_arc) + // .capture_msg_and_let_it_fly_on(), + // )), + // None, + // ); + // subject.scan_schedulers.update_imminent_scheduler( + // ScanType::Payables, + // Some(Box::new( + // NotifyHandleMock::::default() + // .notify_params(¬ify_payable_params_arc) + // .capture_msg_and_let_it_fly_on(), + // )), + // ); + // subject.request_transaction_receipts_sub_opt = + // Some(blockchain_bridge_addr.clone().recipient()); + // subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); + // let subject_addr = subject.start(); + // let subject_subs = Accountant::make_subs_from(&subject_addr); + // + // subject_addr.try_send(ScanForNewPayables{response_skeleton_opt: None}).unwrap(); + // + // subject_addr.try_send(SentPayables{ payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct(pending_payable)]), response_skeleton_opt: None }).unwrap(); + // + // + // send_start_message!(subject_subs); + // + // let time_before = SystemTime::now(); + // system.run(); + // let time_after = SystemTime::now(); + // let tlh = TestLogHandler::new(); + // tlh.assert_logs_contain_in_order(vec![ + // &format!( + // "DEBUG: {test_name}: There was nothing to process during PendingPayables scan." + // ), + // &format!("DEBUG: {test_name}: There was nothing to process during Payables scan."), + // ]); + // let (pending_payable_first_attempt_timestamp, pending_payable_second_attempt_timestamp) = + // assert_start_scan_params_after_called_twice( + // "pending payable", + // test_name, + // &tlh, + // &consuming_wallet, + // time_before, + // time_after, + // &start_scan_pending_payable_params_arc, + // ); + // let (payable_first_attempt_timestamp, payable_second_attempt_timestamp) = + // assert_start_scan_params_after_called_twice( + // "payable", + // test_name, + // &tlh, + // &consuming_wallet, + // time_before, + // time_after, + // &start_scan_payable_params_arc, + // ); + // assert!(pending_payable_first_attempt_timestamp < payable_first_attempt_timestamp); + // assert!(pending_payable_second_attempt_timestamp < payable_second_attempt_timestamp); + // let notify_later_pending_payables_params = + // notify_later_pending_payables_params_arc.lock().unwrap(); + // assert_eq!( + // *notify_later_pending_payables_params, + // vec![ + // ( + // ScanForPendingPayables { + // response_skeleton_opt: None + // }, + // Duration::from_millis(97) + // ), + // ( + // ScanForPendingPayables { + // response_skeleton_opt: None + // }, + // Duration::from_millis(97) + // ), + // ( + // ScanForPendingPayables { + // response_skeleton_opt: None + // }, + // Duration::from_millis(97) + // ), + // ] + // ); + // let notify_payables_params = notify_payable_params_arc.lock().unwrap(); + // assert_eq!( + // *notify_payables_params, + // vec![ + // ScanForNewPayables { + // response_skeleton_opt: None + // }, + // ScanForNewPayables { + // response_skeleton_opt: None + // }, + // ScanForNewPayables { + // response_skeleton_opt: None + // } + // ] + // ); + // let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + // let actual_requested_receipts_1 = + // blockchain_bridge_recording.get_record::(0); + // assert_eq!(actual_requested_receipts_1, &request_transaction_receipts_1); + // let actual_requested_receipts_2 = + // blockchain_bridge_recording.get_record::(1); + // assert_eq!(actual_requested_receipts_2, &request_transaction_receipts_2); + // let actual_qualified_payables = + // blockchain_bridge_recording.get_record::(2); + // assert_eq!(actual_qualified_payables, &qualified_payables_msg) } fn assert_start_scan_params_after_called_twice( @@ -2417,7 +2474,7 @@ mod tests { subject.consuming_wallet_opt = None; subject.logger = Logger::new(test_name); - subject.handle_request_of_scan_for_payable(None); + subject.handle_request_of_scan_for_new_payable(None); let has_scan_started = subject.scanners.payable.scan_started_at().is_some(); assert_eq!(has_scan_started, false); @@ -2451,6 +2508,7 @@ mod tests { let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(100), + pending_payable_scan_interval: Duration::from_millis(50), receivable_scan_interval: Duration::from_millis(100), }); config.suppress_initial_scans = true; @@ -2558,6 +2616,7 @@ mod tests { let consuming_wallet = make_paying_wallet(b"consuming"); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_secs(50_000), + pending_payable_scan_interval: Duration::from_secs(10_000), receivable_scan_interval: Duration::from_secs(50_000), }); let now = to_time_t(SystemTime::now()); @@ -2608,7 +2667,7 @@ mod tests { context_id: 24, }); - subject.handle_request_of_scan_for_payable(response_skeleton_opt); + subject.handle_request_of_scan_for_new_payable(response_skeleton_opt); System::current().stop(); system.run(); @@ -2628,25 +2687,30 @@ mod tests { fn accountant_does_not_initiate_another_scan_if_one_is_already_running() { init_test_logging(); let test_name = "accountant_does_not_initiate_another_scan_if_one_is_already_running"; + let now = SystemTime::now(); + let payment_thresholds = PaymentThresholds::default(); let (blockchain_bridge, _, blockchain_bridge_recording) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge .system_stop_conditions(match_every_type_id!( - RequestTransactionReceipts, - RequestTransactionReceipts + QualifiedPayablesMessage, + QualifiedPayablesMessage )) .start(); - let pps_for_blockchain_bridge_sub = blockchain_bridge_addr.clone().recipient(); - let pending_payable_fingerprint = make_pending_payable_fingerprint(); - let pending_payable_dao = PendingPayableDaoMock::new() - .return_all_errorless_fingerprints_result(vec![pending_payable_fingerprint.clone()]) - .increment_scan_attempts_result(Ok(())) - .return_all_errorless_fingerprints_result(vec![pending_payable_fingerprint.clone()]); - let config = bc_from_earning_wallet(make_wallet("mine")); - let system = System::new(test_name); + let qualified_payables_sub = blockchain_bridge_addr.clone().recipient(); + let (mut qualified_payables, _, _) = + make_qualified_and_unqualified_payables(now, &payment_thresholds); + let payable_1 = qualified_payables.remove(0); + let payable_2 = qualified_payables.remove(0); + let payable_dao = PayableDaoMock::new() + .non_pending_payables_result(vec![payable_1.clone()]) + .non_pending_payables_result(vec![payable_2.clone()]); + let mut config = bc_from_earning_wallet(make_wallet("mine")); + config.payment_thresholds_opt = Some(payment_thresholds); + let system = System::new(test_name); let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) .logger(Logger::new(test_name)) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .payable_daos(vec![ForPayableScanner(payable_dao)]) .bootstrapper_config(config) .build(); let message_before = ScanForNewPayables { @@ -2655,51 +2719,48 @@ mod tests { context_id: 222, }), }; + let message_simultaneous = ScanForNewPayables { + response_skeleton_opt: None, + }; let message_after = ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 333, context_id: 444, }), }; - subject.request_transaction_receipts_sub_opt = Some(pps_for_blockchain_bridge_sub); + subject.qualified_payables_sub_opt = Some(qualified_payables_sub); let addr = subject.start(); addr.try_send(message_before.clone()).unwrap(); - addr.try_send(ScanForNewPayables { - response_skeleton_opt: None, - }) - .unwrap(); + addr.try_send(message_simultaneous).unwrap(); - // We ignored the second ScanForNewPayables message because the first message meant a scan - // was already in progress; now let's reset the state by ending the first scan by a failure - // and see that the third scan attempt will be processed willingly again. - addr.try_send(ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::LocalError("bluh".to_string()), - pending_payable_fingerprint, - )], + // We ignored the second ScanForNewPayables message as there was already in progress from + // the first message. Now we reset the state by ending the first scan by a failure and see + // that the third scan request is gonna be accepted willingly again. + addr.try_send(SentPayables { + payment_procedure_result: Err(PayableTransactionError::Signing("bluh".to_string())), response_skeleton_opt: None, }) .unwrap(); addr.try_send(message_after.clone()).unwrap(); system.run(); let blockchain_bridge_recording = blockchain_bridge_recording.lock().unwrap(); - let messages_received = blockchain_bridge_recording.len(); - assert_eq!(messages_received, 2); - let first_message_actual: &RequestTransactionReceipts = + let first_message_actual: &QualifiedPayablesMessage = blockchain_bridge_recording.get_record(0); assert_eq!( first_message_actual.response_skeleton_opt, message_before.response_skeleton_opt ); - let second_message_actual: &RequestTransactionReceipts = + let second_message_actual: &QualifiedPayablesMessage = blockchain_bridge_recording.get_record(1); assert_eq!( second_message_actual.response_skeleton_opt, message_after.response_skeleton_opt ); + let messages_received = blockchain_bridge_recording.len(); + assert_eq!(messages_received, 2); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {}: PendingPayables scan was already initiated", + "DEBUG: {}: Payables scan was already initiated", test_name )); } @@ -2744,14 +2805,13 @@ mod tests { let account_addr = subject.start(); let _ = account_addr - .try_send(ScanForNewPayables { + .try_send(ScanForPendingPayables { response_skeleton_opt: None, }) .unwrap(); system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!(blockchain_bridge_recording.len(), 1); let received_msg = blockchain_bridge_recording.get_record::(0); assert_eq!( received_msg, @@ -2760,6 +2820,7 @@ mod tests { response_skeleton_opt: None, } ); + assert_eq!(blockchain_bridge_recording.len(), 1); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing("DEBUG: Accountant: Found 2 pending payables to process"); } @@ -3393,375 +3454,355 @@ mod tests { } #[test] - fn pending_transaction_is_registered_and_monitored_until_it_gets_confirmed_or_canceled() { - init_test_logging(); - let port = find_free_port(); - let pending_tx_hash_1 = - H256::from_str("e66814b2812a80d619813f51aa999c0df84eb79d10f4923b2b7667b30d6b33d3") - .unwrap(); - let pending_tx_hash_2 = - H256::from_str("0288ef000581b3bca8a2017eac9aea696366f8f1b7437f18d1aad57bccb7032c") - .unwrap(); - let _blockchain_client_server = MBCSBuilder::new(port) - // Blockchain Agent Gas Price - .ok_response("0x3B9ACA00".to_string(), 0) // 1000000000 - // Blockchain Agent transaction fee balance - .ok_response("0xFFF0".to_string(), 0) // 65520 - // Blockchain Agent masq balance - .ok_response( - "0x000000000000000000000000000000000000000000000000000000000000FFFF".to_string(), - 0, - ) - // Submit payments to blockchain - .ok_response("0xFFF0".to_string(), 1) - .begin_batch() - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_1) - .build(), - ) - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .build(), - ) - .end_batch() - // Round 1 - handle_request_transaction_receipts - .begin_batch() - .raw_response(r#"{ "jsonrpc": "2.0", "id": 1, "result": null }"#.to_string()) // Null response - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .build(), - ) - .end_batch() - // Round 2 - handle_request_transaction_receipts - .begin_batch() - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_1) - .build(), - ) - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .build(), - ) - .end_batch() - // Round 3 - handle_request_transaction_receipts - .begin_batch() - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_1) - .status(U64::from(0)) - .build(), - ) - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .build(), - ) - .end_batch() - // Round 4 - handle_request_transaction_receipts - .begin_batch() - .raw_response( - ReceiptResponseBuilder::default() - .transaction_hash(pending_tx_hash_2) - .status(U64::from(1)) - .block_number(U64::from(1234)) - .block_hash(Default::default()) - .build(), - ) - .end_batch() - .start(); - let non_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); - let mark_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); - let return_all_errorless_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); - let update_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); - let mark_failure_params_arc = Arc::new(Mutex::new(vec![])); - let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let delete_record_params_arc = Arc::new(Mutex::new(vec![])); - let notify_later_scan_for_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); - let notify_later_scan_for_pending_payable_arc_cloned = - notify_later_scan_for_pending_payable_params_arc.clone(); // because it moves into a closure - let rowid_for_account_1 = 3; - let rowid_for_account_2 = 5; - let now = SystemTime::now(); - let past_payable_timestamp_1 = now.sub(Duration::from_secs( - (DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 555) as u64, - )); - let past_payable_timestamp_2 = now.sub(Duration::from_secs( - (DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 50) as u64, - )); - let this_payable_timestamp_1 = now; - let this_payable_timestamp_2 = now.add(Duration::from_millis(50)); - let payable_account_balance_1 = - gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 10); - let payable_account_balance_2 = - gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 666); - let wallet_account_1 = make_wallet("creditor1"); - let wallet_account_2 = make_wallet("creditor2"); - let blockchain_interface = make_blockchain_interface_web3(port); - let consuming_wallet = make_paying_wallet(b"wallet"); - let system = System::new("pending_transaction"); - let persistent_config_id_stamp = ArbitraryIdStamp::new(); - let persistent_config = PersistentConfigurationMock::default() - .set_arbitrary_id_stamp(persistent_config_id_stamp); - let blockchain_bridge = BlockchainBridge::new( - Box::new(blockchain_interface), - Arc::new(Mutex::new(persistent_config)), - false, + fn accountant_processes_sent_payables_and_schedules_pending_payable_scanner() { + let fingerprints_rowids_params_arc = Arc::new(Mutex::new(vec![])); + let mark_pending_payables_rowids_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let expected_wallet = make_wallet("paying_you"); + let expected_hash = H256::from("transaction_hash".keccak256()); + let expected_rowid = 45623; + let pending_payable_dao = PendingPayableDaoMock::default() + .fingerprints_rowids_params(&fingerprints_rowids_params_arc) + .fingerprints_rowids_result(TransactionHashes { + rowid_results: vec![(expected_rowid, expected_hash)], + no_rowid_results: vec![], + }); + let payable_dao = PayableDaoMock::new() + .mark_pending_payables_rowids_params(&mark_pending_payables_rowids_params_arc) + .mark_pending_payables_rowids_result(Ok(())); + let system = System::new("accountant_calls_payable_dao_to_mark_pending_payable"); + let mut subject = AccountantBuilder::default() + .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) + .payable_daos(vec![ForPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) + .build(); + let pending_payable_interval = Duration::from_millis(55); + subject.scan_schedulers.pending_payable.interval = pending_payable_interval; + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&pending_payable_notify_later_params_arc), ); - let account_1 = PayableAccount { - wallet: wallet_account_1.clone(), - balance_wei: payable_account_balance_1, - last_paid_timestamp: past_payable_timestamp_1, - pending_payable_opt: None, - }; - let account_2 = PayableAccount { - wallet: wallet_account_2.clone(), - balance_wei: payable_account_balance_2, - last_paid_timestamp: past_payable_timestamp_2, - pending_payable_opt: None, + let expected_payable = PendingPayable::new(expected_wallet.clone(), expected_hash.clone()); + let sent_payable = SentPayables { + payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( + expected_payable.clone(), + )]), + response_skeleton_opt: None, }; - let payable_scan_interval = 1000; // should be slightly less than 1/5 of the time until shutting the system - let payable_dao_for_payable_scanner = PayableDaoMock::new() - .non_pending_payables_params(&non_pending_payables_params_arc) - .non_pending_payables_result(vec![account_1, account_2]) - .mark_pending_payables_rowids_params(&mark_pending_payable_params_arc) - .mark_pending_payables_rowids_result(Ok(())); - let payable_dao_for_pending_payable_scanner = PayableDaoMock::new() + let addr = subject.start(); + + addr.try_send(sent_payable).expect("unexpected actix error"); + + System::current().stop(); + system.run(); + let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); + assert_eq!(*fingerprints_rowids_params, vec![vec![expected_hash]]); + let mark_pending_payables_rowids_params = + mark_pending_payables_rowids_params_arc.lock().unwrap(); + assert_eq!( + *mark_pending_payables_rowids_params, + vec![vec![(expected_wallet, expected_rowid)]] + ); + let pending_payable_notify_later_params = + pending_payable_notify_later_params_arc.lock().unwrap(); + assert_eq!( + *pending_payable_notify_later_params, + vec![(ScanForPendingPayables::default(), pending_payable_interval)] + ); + // The accountant is unbound here. We don't use the bind message. It means we can prove + // none of those other scan requests could have been sent (especially ScanForNewPayables, + // ScanForRetryPayables) + } + + #[test] + fn accountant_schedule_another_retry_payable_scanner_because_some_pending_payables_did_not_complete( + ) { + let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default() .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); - let mut bootstrapper_config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - bootstrapper_config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(payable_scan_interval), // we don't care about this scan - receivable_scan_interval: Duration::from_secs(1_000_000), // we don't care about this scan - }); - let fingerprint_1_first_round = PendingPayableFingerprint { - rowid: rowid_for_account_1, - timestamp: this_payable_timestamp_1, - hash: pending_tx_hash_1, - attempt: 1, - amount: payable_account_balance_1, - process_error: None, - }; - let fingerprint_2_first_round = PendingPayableFingerprint { - rowid: rowid_for_account_2, - timestamp: this_payable_timestamp_2, - hash: pending_tx_hash_2, - attempt: 1, - amount: payable_account_balance_2, - process_error: None, - }; - let fingerprint_1_second_round = PendingPayableFingerprint { - attempt: 2, - ..fingerprint_1_first_round.clone() - }; - let fingerprint_2_second_round = PendingPayableFingerprint { - attempt: 2, - ..fingerprint_2_first_round.clone() - }; - let fingerprint_1_third_round = PendingPayableFingerprint { - attempt: 3, - ..fingerprint_1_first_round.clone() - }; - let fingerprint_2_third_round = PendingPayableFingerprint { - attempt: 3, - ..fingerprint_2_first_round.clone() - }; - let fingerprint_2_fourth_round = PendingPayableFingerprint { - attempt: 4, - ..fingerprint_2_first_round.clone() - }; - let pending_payable_dao_for_payable_scanner = PendingPayableDaoMock::default() - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (rowid_for_account_1, pending_tx_hash_1), - (rowid_for_account_2, pending_tx_hash_2), - ], - no_rowid_results: vec![], - }) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (rowid_for_account_1, pending_tx_hash_1), - (rowid_for_account_2, pending_tx_hash_2), - ], - no_rowid_results: vec![], - }); - let mut pending_payable_dao_for_pending_payable_scanner = PendingPayableDaoMock::new() - .return_all_errorless_fingerprints_params(&return_all_errorless_fingerprints_params_arc) - .return_all_errorless_fingerprints_result(vec![]) - .return_all_errorless_fingerprints_result(vec![ - fingerprint_1_first_round, - fingerprint_2_first_round, - ]) - .return_all_errorless_fingerprints_result(vec![ - fingerprint_1_second_round, - fingerprint_2_second_round, - ]) - .return_all_errorless_fingerprints_result(vec![ - fingerprint_1_third_round, - fingerprint_2_third_round, - ]) - .return_all_errorless_fingerprints_result(vec![fingerprint_2_fourth_round.clone()]) - .fingerprints_rowids_result(TransactionHashes { - rowid_results: vec![ - (rowid_for_account_1, pending_tx_hash_1), - (rowid_for_account_2, pending_tx_hash_2), - ], - no_rowid_results: vec![], - }) - .increment_scan_attempts_params(&update_fingerprint_params_arc) - .increment_scan_attempts_result(Ok(())) - .increment_scan_attempts_result(Ok(())) - .increment_scan_attempts_result(Ok(())) - .mark_failures_params(&mark_failure_params_arc) - // we don't have a better solution yet, so we mark this down - .mark_failures_result(Ok(())) - .delete_fingerprints_params(&delete_record_params_arc) - // this is used during confirmation of the successful one - .delete_fingerprints_result(Ok(())); - pending_payable_dao_for_pending_payable_scanner - .have_return_all_errorless_fingerprints_shut_down_the_system = true; - let pending_payable_dao_for_accountant = - PendingPayableDaoMock::new().insert_fingerprints_result(Ok(())); - let accountant_addr = Arbiter::builder() - .stop_system_on_panic(true) - .start(move |_| { - let mut subject = AccountantBuilder::default() - .consuming_wallet(consuming_wallet) - .bootstrapper_config(bootstrapper_config) - .payable_daos(vec![ - ForPayableScanner(payable_dao_for_payable_scanner), - ForPendingPayableScanner(payable_dao_for_pending_payable_scanner), - ]) - .pending_payable_daos(vec![ - ForAccountantBody(pending_payable_dao_for_accountant), - ForPayableScanner(pending_payable_dao_for_payable_scanner), - ForPendingPayableScanner(pending_payable_dao_for_pending_payable_scanner), - ]) - .build(); - subject.scanners.receivable = Box::new(NullScanner::new()); - let notify_later_half_mock = NotifyLaterHandleMock::default() - .notify_later_params(¬ify_later_scan_for_pending_payable_arc_cloned) - .capture_msg_and_let_it_fly_on(); - subject.scan_schedulers.update_periodical_scheduler( - ScanType::Payables, - Some(Box::new(notify_later_half_mock)), - None, - ); - subject - }); - let mut peer_actors = peer_actors_builder().build(); - let accountant_subs = Accountant::make_subs_from(&accountant_addr); - peer_actors.accountant = accountant_subs.clone(); - let blockchain_bridge_addr = blockchain_bridge.start(); - let blockchain_bridge_subs = BlockchainBridge::make_subs_from(&blockchain_bridge_addr); - peer_actors.blockchain_bridge = blockchain_bridge_subs.clone(); - send_bind_message!(accountant_subs, peer_actors); - send_bind_message!(blockchain_bridge_subs, peer_actors); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let mut subject = AccountantBuilder::default() + .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let system = System::new("some_pending_payables_did_not_complete"); + let pending_payable_interval = Duration::from_millis(12); + subject.scan_schedulers.pending_payable.interval = pending_payable_interval; + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&pending_payable_notify_later_params_arc), + ); + let (msg, two_fingerprints) = make_report_transaction_receipts_msg( + TxStatus::Succeeded(TransactionBlock { + block_hash: Default::default(), + block_number: U64::from(100), + }), + TxStatus::Failed, + ); + let subject_addr = subject.start(); - send_start_message!(accountant_subs); + subject_addr.try_send(msg).unwrap(); - assert_eq!(system.run(), 0); - let mut mark_pending_payable_params = mark_pending_payable_params_arc.lock().unwrap(); - let mut one_set_of_mark_pending_payable_params = mark_pending_payable_params.remove(0); - assert!(mark_pending_payable_params.is_empty()); - let first_payable = one_set_of_mark_pending_payable_params.remove(0); - assert_eq!(first_payable.0, wallet_account_1); - assert_eq!(first_payable.1, rowid_for_account_1); - let second_payable = one_set_of_mark_pending_payable_params.remove(0); - assert!( - one_set_of_mark_pending_payable_params.is_empty(), - "{:?}", - one_set_of_mark_pending_payable_params + System::current().stop(); + system.run(); + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!( + *transactions_confirmed_params, + vec![vec![two_fingerprints[1].clone()]] ); - assert_eq!(second_payable.0, wallet_account_2); - assert_eq!(second_payable.1, rowid_for_account_2); - let return_all_errorless_fingerprints_params = - return_all_errorless_fingerprints_params_arc.lock().unwrap(); - // it varies with machines and sometimes we manage more cycles than necessary - assert!(return_all_errorless_fingerprints_params.len() >= 5); - let non_pending_payables_params = non_pending_payables_params_arc.lock().unwrap(); - assert_eq!(*non_pending_payables_params, vec![()]); // because we disabled further scanning for payables - let update_fingerprints_params = update_fingerprint_params_arc.lock().unwrap(); + let pending_payable_notify_later_params = + pending_payable_notify_later_params_arc.lock().unwrap(); assert_eq!( - *update_fingerprints_params, - vec![ - vec![rowid_for_account_1, rowid_for_account_2], - vec![rowid_for_account_1, rowid_for_account_2], - vec![rowid_for_account_2], - ] + *pending_payable_notify_later_params, + vec![(ScanForPendingPayables::default(), pending_payable_interval)] + ); + } + + #[test] + fn accountant_schedule_another_retry_payable_scanner_because_all_pending_payables_still_pending( + ) { + let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let mut subject = AccountantBuilder::default() + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let system = System::new("all_pending_payables_still_pending"); + let pending_payable_interval = Duration::from_millis(23); + subject.scan_schedulers.pending_payable.interval = pending_payable_interval; + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&pending_payable_notify_later_params_arc), ); - let mark_failure_params = mark_failure_params_arc.lock().unwrap(); - assert_eq!(*mark_failure_params, vec![vec![rowid_for_account_1]]); - let delete_record_params = delete_record_params_arc.lock().unwrap(); - assert_eq!(*delete_record_params, vec![vec![rowid_for_account_2]]); - let transaction_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + let (msg, _) = make_report_transaction_receipts_msg(TxStatus::Pending, TxStatus::Pending); + let subject_addr = subject.start(); + + subject_addr.try_send(msg).unwrap(); + + System::current().stop(); + system.run(); + let pending_payable_notify_later_params = + pending_payable_notify_later_params_arc.lock().unwrap(); assert_eq!( - *transaction_confirmed_params, - vec![vec![fingerprint_2_fourth_round.clone()]] + *pending_payable_notify_later_params, + vec![(ScanForPendingPayables::default(), pending_payable_interval)] ); - let expected_scan_for_payable_msg_and_interval = ( - ScanForNewPayables { - response_skeleton_opt: None, - }, - Duration::from_millis(payable_scan_interval), + // The payable DAO wasn't prepared in this test and therefore any payment confirmation did + // not take place + } + + #[test] + fn accountant_confirms_payable_txs_and_schedules_the_new_payable_scanner_timely() { + let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); + let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); + let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default() + .transactions_confirmed_params(&transactions_confirmed_params_arc) + .transactions_confirmed_result(Ok(())); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let system = System::new("new_payable_scanner_timely"); + let mut subject = AccountantBuilder::default() + .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let last_new_payable_scan_timestamp = SystemTime::now() + .checked_sub(Duration::from_secs(3)) + .unwrap(); + let nominal_interval = Duration::from_secs(6); + let expected_computed_interval = Duration::from_secs(3); + let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() + .compute_interval_params(&compute_interval_params_arc) + .compute_interval_results(Some(expected_computed_interval)); + subject.scan_schedulers.payable.nominal_interval = nominal_interval; + subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); + subject + .scan_schedulers + .payable + .last_new_payable_scan_timestamp + .replace(last_new_payable_scan_timestamp); + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); - let mut notify_later_check_for_confirmation = - notify_later_scan_for_pending_payable_params_arc - .lock() - .unwrap(); - // it varies with machines and sometimes we manage more cycles than necessary - let vector_of_first_five_cycles = notify_later_check_for_confirmation - .drain(0..=4) - .collect_vec(); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); + let subject_addr = subject.start(); + let (msg, two_fingerprints) = make_report_transaction_receipts_msg( + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(123), + block_number: U64::from(100), + }), + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(234), + block_number: U64::from(200), + }), + ); + + subject_addr.try_send(msg).unwrap(); + + System::current().stop(); + system.run(); + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!(*transactions_confirmed_params, vec![two_fingerprints]); + let compute_interval_params = compute_interval_params_arc.lock().unwrap(); assert_eq!( - vector_of_first_five_cycles, - vec![ - expected_scan_for_payable_msg_and_interval.clone(), - expected_scan_for_payable_msg_and_interval.clone(), - expected_scan_for_payable_msg_and_interval.clone(), - expected_scan_for_payable_msg_and_interval.clone(), - expected_scan_for_payable_msg_and_interval, - ] + *compute_interval_params, + vec![(last_new_payable_scan_timestamp, nominal_interval)] ); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing( - "WARN: Accountant: Broken transactions 0xe66814b2812a80d619813f51aa999c0df84eb79d10f\ - 4923b2b7667b30d6b33d3 marked as an error. You should take over the care of those to make sure \ - your debts are going to be settled properly. At the moment, there is no automated process \ - fixing that without your assistance"); - log_handler.exists_log_matching("INFO: Accountant: Transaction 0x0288ef000581b3bca8a2017eac9\ - aea696366f8f1b7437f18d1aad57bccb7032c has been added to the blockchain; detected locally at \ - attempt 4 at \\d{2,}ms after its sending"); - log_handler.exists_log_containing( - "INFO: Accountant: Transactions 0x0288ef000581b3bca8a2017eac9aea696366f8f1b7437f18d1aad5\ - 7bccb7032c completed their confirmation process succeeding", + let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); + assert_eq!( + *new_payable_notify_later, + vec![(ScanForNewPayables::default(), expected_computed_interval)] ); + let new_payable_notify = new_payable_notify_arc.lock().unwrap(); + assert!( + new_payable_notify.is_empty(), + "should be empty but was: {:?}", + new_payable_notify + ) } #[test] - fn accountant_receives_reported_transaction_receipts_and_processes_them_all() { + fn accountant_confirms_payable_txs_and_schedules_the_delayed_new_payable_scanner_asap() { let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); + let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); + let new_payable_notify_arc = Arc::new(Mutex::new(vec![])); let payable_dao = PayableDaoMock::default() .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); - let subject = AccountantBuilder::default() + let mut subject = AccountantBuilder::default() + .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let last_new_payable_scan_timestamp = SystemTime::now() + .checked_sub(Duration::from_secs(8)) + .unwrap(); + let nominal_interval = Duration::from_secs(6); + let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() + .compute_interval_params(&compute_interval_params_arc) + .compute_interval_results(None); + subject.scan_schedulers.payable.nominal_interval = nominal_interval; + subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); + subject + .scan_schedulers + .payable + .last_new_payable_scan_timestamp + .replace(last_new_payable_scan_timestamp); + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), + ); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); + let subject_addr = subject.start(); + let (msg, two_fingerprints) = make_report_transaction_receipts_msg( + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(123), + block_number: U64::from(100), + }), + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(234), + block_number: U64::from(200), + }), + ); + + subject_addr.try_send(msg).unwrap(); + + let system = System::new("new_payable_scanner_asap"); + System::current().stop(); + system.run(); + let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); + assert_eq!(*transactions_confirmed_params, vec![two_fingerprints]); + let compute_interval_params = compute_interval_params_arc.lock().unwrap(); + assert_eq!( + *compute_interval_params, + vec![(last_new_payable_scan_timestamp, nominal_interval)] + ); + let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); + assert!( + new_payable_notify_later.is_empty(), + "should be empty but was: {:?}", + new_payable_notify_later + ); + let new_payable_notify = new_payable_notify_arc.lock().unwrap(); + assert_eq!(*new_payable_notify, vec![ScanForNewPayables::default()]) + } + + #[test] + fn scheduler_for_new_payables_computes_with_appropriate_now_timestamp() { + let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); + let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let system = + System::new("scheduler_for_new_payables_computes_with_appropriate_now_timestamp"); + let mut subject = AccountantBuilder::default() .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .build(); + let last_new_payable_scan_timestamp = SystemTime::now() + .checked_sub(Duration::from_millis(3500)) + .unwrap(); + let nominal_interval = Duration::from_secs(6); + subject.scan_schedulers.payable.nominal_interval = nominal_interval; + subject + .scan_schedulers + .payable + .last_new_payable_scan_timestamp + .replace(last_new_payable_scan_timestamp); + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), + ); let subject_addr = subject.start(); + let (msg, _) = make_report_transaction_receipts_msg( + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(123), + block_number: U64::from(100), + }), + TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(234), + block_number: U64::from(200), + }), + ); + + subject_addr.try_send(msg).unwrap(); + + let before = SystemTime::now(); + System::current().stop(); + system.run(); + let after = SystemTime::now(); + let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); + let (_, actual_interval) = new_payable_notify_later[0]; + let interval_computer = NewPayableScanDynIntervalComputerReal::default(); + let left_side_bound = interval_computer + .compute_interval(before, last_new_payable_scan_timestamp, nominal_interval) + .unwrap(); + let right_side_bound = interval_computer + .compute_interval(after, last_new_payable_scan_timestamp, nominal_interval) + .unwrap(); + assert!( + left_side_bound >= actual_interval && actual_interval >= right_side_bound, + "expected actual {:?} to be between {:?} and {:?}", + actual_interval, + left_side_bound, + right_side_bound + ); + } + + fn make_report_transaction_receipts_msg( + status_of_tx_1: TxStatus, + status_of_tx_2: TxStatus, + ) -> (ReportTransactionReceipts, [PendingPayableFingerprint; 2]) { let transaction_hash_1 = make_tx_hash(4545); let transaction_receipt_1 = TxReceipt { transaction_hash: transaction_hash_1, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number: U64::from(100), - }), + status: status_of_tx_1, }; let fingerprint_1 = PendingPayableFingerprint { rowid: 5, @@ -3774,10 +3815,7 @@ mod tests { let transaction_hash_2 = make_tx_hash(3333333); let transaction_receipt_2 = TxReceipt { transaction_hash: transaction_hash_2, - status: TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number: U64::from(200), - }), + status: status_of_tx_2, }; let fingerprint_2 = PendingPayableFingerprint { rowid: 10, @@ -3800,17 +3838,7 @@ mod tests { ], response_skeleton_opt: None, }; - - subject_addr.try_send(msg).unwrap(); - - let system = System::new("processing reported receipts"); - System::current().stop(); - system.run(); - let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!( - *transactions_confirmed_params, - vec![vec![fingerprint_1, fingerprint_2]] - ); + (msg, [fingerprint_1, fingerprint_2]) } #[test] diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 49425ca64..756a2f5c6 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. pub mod payable_scanner; +pub mod scan_schedulers; pub mod scanners_utils; pub mod test_utils; @@ -19,39 +20,36 @@ use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ }; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport}; use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; -use crate::accountant::{PendingPayableId, ScanForPendingPayables, ScanForRetryPayables, SettableSkeletonOptHolder}; +use crate::accountant::{PendingPayableId, ScanForPendingPayables, ScanForRetryPayables}; use crate::accountant::{ - comma_joined_stringifiable, gwei_to_wei, Accountant, ReceivedPayments, + comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, ScanForReceivables, SentPayables, }; use crate::accountant::db_access_objects::banned_dao::BannedDao; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; use crate::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, ScanIntervals, + DaoFactories, FinancialStatistics, PaymentThresholds, }; -use crate::sub_lib::blockchain_bridge::{ - OutboundPaymentsInstructions, -}; -use crate::sub_lib::utils::{NotifyHandle, NotifyHandleReal, NotifyLaterHandle, NotifyLaterHandleReal}; +use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::wallet::Wallet; -use actix::{Context, Message}; +use actix::{Message}; use itertools::{Either, Itertools}; use masq_lib::logger::Logger; use masq_lib::logger::TIME_FORMATTING_STRING; -use masq_lib::messages::{CommendableScanType, ToMessageBody, UiScanResponse}; +use masq_lib::messages::{ToMessageBody, UiScanResponse}; use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; use std::rc::Rc; -use std::time::{Duration, SystemTime}; +use std::time::{SystemTime}; use time::format_description::parse; use time::OffsetDateTime; use web3::types::H256; use masq_lib::type_obfuscation::Obfuscated; -use crate::accountant::scanners::payable_scanner::{PreparedAdjustment, MultistageDualPayableScanner, SolvencySensitivePaymentInstructor}; +use crate::accountant::scanners::payable_scanner::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; use crate::accountant::scanners::payable_scanner::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; @@ -244,7 +242,7 @@ where EndMessage: Message, { // fn scan_starter(starter: &mut dyn InaccessibleScanner) -> PrivateScanStarter where Self: Sized; - fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> Option; + fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> UiScanResult; fn scan_started_at(&self) -> Option; fn mark_as_started(&mut self, timestamp: SystemTime); fn mark_as_ended(&mut self, logger: &Logger); @@ -253,6 +251,22 @@ where as_any_mut_in_trait!(); } +#[derive(Debug, PartialEq)] +pub enum UiScanResult { + Finished(Option), + FollowedByAnotherScan, +} + +impl UiScanResult { + pub fn finished(self) -> Option { + if let UiScanResult::Finished(ui_msg_opt) = self { + ui_msg_opt + } else { + todo!("test drive a panic...") + } + } +} + // Using this Access token to screen away a private interface from its public counterpart, because // Rust doesn't allow selective private methods within a public trait pub struct PrivateScanStarter<'s, TriggerMessage, StartMessage> { @@ -401,10 +415,10 @@ impl InaccessibleScanner for Payab impl InaccessibleScanner for PayableScanner { fn start_scan( &mut self, - consuming_wallet: &Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, + _consuming_wallet: &Wallet, + _timestamp: SystemTime, + _response_skeleton_opt: Option, + _logger: &Logger, ) -> Result { todo!() } @@ -425,7 +439,7 @@ impl ScanWithStarter for Payable } impl AccessibleScanner for PayableScanner { - fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> Option { + fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> UiScanResult { let (sent_payables, err_opt) = separate_errors(&message, logger); debug!( logger, @@ -439,12 +453,13 @@ impl AccessibleScanner for PayableScanne self.handle_sent_payable_errors(err_opt, logger); self.mark_as_ended(logger); - message - .response_skeleton_opt - .map(|response_skeleton| NodeToUiMessage { + + UiScanResult::Finished(message.response_skeleton_opt.map(|response_skeleton| { + NodeToUiMessage { target: MessageTarget::ClientId(response_skeleton.client_id), body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) + } + })) } time_marking_methods!(Payables); @@ -807,15 +822,24 @@ impl InaccessibleScanner impl AccessibleScanner for PendingPayableScanner { - fn finish_scan( - &mut self, - message: ReportTransactionReceipts, - logger: &Logger, - ) -> Option { - let response_skeleton_opt = message.response_skeleton_opt; + fn finish_scan(&mut self, message: ReportTransactionReceipts, logger: &Logger) -> UiScanResult { + let construct_msg_scan_ended_to_ui = move || { + message + .response_skeleton_opt + .map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }) + }; match message.fingerprints_with_receipts.is_empty() { - true => debug!(logger, "No transaction receipts found."), + // TODO changed to error log, find a place where it's gonna be tested + true => { + error!(logger, "No transaction receipts found."); + todo!("see if the test assert on the mark_as_ended"); + self.mark_as_ended(logger); + UiScanResult::Finished(construct_msg_scan_ended_to_ui()) + } false => { debug!( logger, @@ -823,16 +847,16 @@ impl AccessibleScanner message.fingerprints_with_receipts.len() ); let scan_report = self.handle_receipts_for_pending_transactions(message, logger); - self.process_transactions_by_reported_state(scan_report, logger); + let requires_payments_retry = + self.process_transactions_by_reported_state(scan_report, logger); + + if requires_payments_retry { + todo!() + } else { + UiScanResult::Finished(construct_msg_scan_ended_to_ui()) + } } } - - self.mark_as_ended(logger); - //TODO remove this - response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) } time_marking_methods!(PendingPayables); @@ -894,10 +918,14 @@ impl PendingPayableScanner { &mut self, scan_report: PendingPayableScanReport, logger: &Logger, - ) { + ) -> bool { + let requires_payments_retry = scan_report.requires_payments_retry(); + self.confirm_transactions(scan_report.confirmed, logger); self.cancel_failed_transactions(scan_report.failures, logger); - self.update_remaining_fingerprints(scan_report.still_pending, logger) + self.update_remaining_fingerprints(scan_report.still_pending, logger); + + requires_payments_retry } fn update_remaining_fingerprints(&self, ids: Vec, logger: &Logger) { @@ -1053,14 +1081,17 @@ impl AccessibleScanner for ReceivableSca // }) // } - fn finish_scan(&mut self, msg: ReceivedPayments, logger: &Logger) -> Option { + fn finish_scan(&mut self, msg: ReceivedPayments, logger: &Logger) -> UiScanResult { self.handle_new_received_payments(&msg, logger); self.mark_as_ended(logger); - msg.response_skeleton_opt - .map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) + + UiScanResult::Finished( + msg.response_skeleton_opt + .map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }), + ) } time_marking_methods!(Receivables); @@ -1257,115 +1288,26 @@ impl BeginScanError { } } -pub struct ScanSchedulers { - pub schedulers: HashMap>, -} - -impl ScanSchedulers { - pub fn new(scan_intervals: ScanIntervals) -> Self { - let schedulers = HashMap::from_iter([ - ( - ScanType::PendingPayables, - Box::new(PeriodicalScanScheduler:: { - handle: Box::new(NotifyLaterHandleReal::default()), - interval: scan_intervals.payable_scan_interval, - }) as Box, - ), - ( - ScanType::Payables, - Box::new(ImminentScanScheduler:: { - handle: Box::new(NotifyHandleReal::default()), - }), - ), - ( - ScanType::Receivables, - Box::new(PeriodicalScanScheduler:: { - handle: Box::new(NotifyLaterHandleReal::default()), - interval: scan_intervals.receivable_scan_interval, - }), - ), - ]); - ScanSchedulers { schedulers } - } -} - -pub trait ScanScheduler { - fn schedule( - &self, - ctx: &mut Context, - response_skeleton_opt: Option, - ); - as_any_ref_in_trait!(); - as_any_mut_in_trait!(); -} - -pub struct PeriodicalScanScheduler { - pub handle: Box>, - pub interval: Duration, -} - -impl ScanScheduler for PeriodicalScanScheduler { - fn schedule( - &self, - ctx: &mut Context, - _response_skeleton_opt: Option, - ) { - // the default of the message implies response_skeleton_opt to be None - // because scheduled scans don't respond - let _ = self - .handle - .notify_later(Message::default(), self.interval, ctx); - } - as_any_ref_in_trait_impl!(); - as_any_mut_in_trait_impl!(); -} - -pub struct ImminentScanScheduler { - pub handle: Box>, -} - -impl ScanScheduler - for ImminentScanScheduler -{ - fn schedule( - &self, - ctx: &mut Context, - response_skeleton_opt: Option, - ) { - let mut msg = Message::default(); - if let Some(skeleton) = response_skeleton_opt { - todo!() - //msg.set_skeleton(skeleton) - } - let _ = self.handle.notify(Message::default(), ctx); - } - as_any_ref_in_trait_impl!(); - as_any_mut_in_trait_impl!(); -} - +// Note that this location was chosen because these mocks below need to implement a private trait +// from this file #[cfg(test)] pub mod local_test_utils { use crate::accountant::scanners::payable_scanner::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::{ - AccessibleScanner, BeginScanError, ImminentScanScheduler, InaccessibleScanner, - MultistageDualPayableScanner, PayableScanner, PeriodicalScanScheduler, PreparedAdjustment, - PrivateScanStarter, ScanSchedulers, ScanType, ScanWithStarter, - SolvencySensitivePaymentInstructor, StartableAccessibleScanner, + AccessibleScanner, BeginScanError, InaccessibleScanner, MultistageDualPayableScanner, + PreparedAdjustment, PrivateScanStarter, ScanWithStarter, + SolvencySensitivePaymentInstructor, StartableAccessibleScanner, UiScanResult, }; + use crate::accountant::BlockchainAgentWithContextMessage; use crate::accountant::OutboundPaymentsInstructions; - use crate::accountant::{Accountant, ResponseSkeleton, SentPayables}; - use crate::accountant::{BlockchainAgentWithContextMessage, ScanForNewPayables}; - use crate::sub_lib::peer_actors::StartMessage; - use crate::sub_lib::utils::{NotifyHandle, NotifyLaterHandle}; + use crate::accountant::{ResponseSkeleton, SentPayables}; use crate::sub_lib::wallet::Wallet; use actix::{Message, System}; use itertools::Either; use masq_lib::logger::Logger; - use masq_lib::messages::CommendableScanType; - use masq_lib::ui_gateway::NodeToUiMessage; use std::cell::RefCell; use std::sync::{Arc, Mutex}; - use std::time::{Duration, SystemTime}; + use std::time::SystemTime; macro_rules! formal_traits_for_payable_mid_scan_msg_handling { ($scanner:ty) => { @@ -1417,11 +1359,7 @@ pub mod local_test_utils { StartMessage: Message, EndMessage: Message, { - fn finish_scan( - &mut self, - _message: EndMessage, - _logger: &Logger, - ) -> Option { + fn finish_scan(&mut self, _message: EndMessage, _logger: &Logger) -> UiScanResult { panic!("Called finish_scan() from NullScanner"); } @@ -1449,10 +1387,10 @@ pub mod local_test_utils { { fn start_scan( &mut self, - wallet: &Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, + _wallet: &Wallet, + _timestamp: SystemTime, + _response_skeleton_opt: Option, + _logger: &Logger, ) -> Result { Err(BeginScanError::CalledFromNullScanner) } @@ -1474,7 +1412,7 @@ pub mod local_test_utils { start_scan_params: Arc, Logger)>>>, start_scan_results: RefCell>>, end_scan_params: Arc>>, - end_scan_results: RefCell>>, + end_scan_results: RefCell>, started_at_results: RefCell>>, stop_system_after_last_message: RefCell, } @@ -1497,7 +1435,7 @@ pub mod local_test_utils { EndMessage: Message, { fn scan_starter(&mut self) -> PrivateScanStarter { - todo!() + PrivateScanStarter::new(self) } } @@ -1534,11 +1472,7 @@ pub mod local_test_utils { StartMessage: Message, EndMessage: Message, { - fn finish_scan( - &mut self, - message: EndMessage, - _logger: &Logger, - ) -> Option { + fn finish_scan(&mut self, message: EndMessage, _logger: &Logger) -> UiScanResult { self.end_scan_params.lock().unwrap().push(message); if self.is_allowed_to_stop_the_system() && self.is_last_message() { System::current().stop(); @@ -1618,46 +1552,6 @@ pub mod local_test_utils { } formal_traits_for_payable_mid_scan_msg_handling!(ScannerMock); - - impl ScanSchedulers { - pub fn update_periodical_scheduler( - &mut self, - scan_type: ScanType, - handle_opt: Option>>, - interval_opt: Option, - ) { - let scheduler: &mut PeriodicalScanScheduler = self.scheduler_mut(scan_type); - if let Some(new_handle) = handle_opt { - scheduler.handle = new_handle - } - if let Some(new_interval) = interval_opt { - scheduler.interval = new_interval - } - } - - pub fn update_imminent_scheduler( - &mut self, - scan_type: ScanType, - handle_opt: Option>>, - ) { - let scheduler: &mut ImminentScanScheduler = self.scheduler_mut(scan_type); - if let Some(new_handle) = handle_opt { - scheduler.handle = new_handle - } - } - - fn scheduler_mut( - &mut self, - scan_type: ScanType, - ) -> &mut SchedulerType { - self.schedulers - .get_mut(&scan_type) - .unwrap() - .as_any_mut() - .downcast_mut::() - .unwrap() - } - } } #[cfg(test)] @@ -1671,9 +1565,9 @@ mod tests { use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PendingPayableMetadata; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; use crate::accountant::scanners::test_utils::protect_payables_in_test; - use crate::accountant::scanners::{BeginScanError, PendingPayableScanner, ReceivableScanner, ScanSchedulers, ScanType, AccessibleScanner, ScannerCommon, Scanners, InaccessibleScanner, PeriodicalScanScheduler, ImminentScanScheduler, PayableScanner, ScanWithStarter, PrivateScanStarter}; - use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; - use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanForNewPayables, ScanForPendingPayables, ScanForReceivables, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; + use crate::accountant::scanners::{AccessibleScanner, BeginScanError, InaccessibleScanner, PayableScanner, PendingPayableScanner, PrivateScanStarter, ReceivableScanner, ScanType, ScanWithStarter, ScannerCommon, Scanners}; + use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; + use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanForNewPayables, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ @@ -1683,9 +1577,9 @@ mod tests { use crate::database::rusqlite_wrappers::TransactionSafeWrapper; use crate::database::test_utils::transaction_wrapper_mock::TransactionInnerWrapperMockBuilder; use crate::db_config::mocks::ConfigDaoMock; - use crate::db_config::persistent_configuration::{PersistentConfigError}; + use crate::db_config::persistent_configuration::PersistentConfigError; use crate::sub_lib::accountant::{ - DaoFactories, FinancialStatistics, PaymentThresholds, ScanIntervals, + DaoFactories, FinancialStatistics, PaymentThresholds, DEFAULT_PAYMENT_THRESHOLDS, }; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; @@ -1827,7 +1721,7 @@ mod tests { let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); let (qualified_payable_accounts, _, all_non_pending_payables) = - make_payables(now, &PaymentThresholds::default()); + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); let mut subject = make_dull_subject(); @@ -1867,8 +1761,10 @@ mod tests { #[test] fn new_payable_scanner_cannot_be_initiated_if_it_is_already_running() { let consuming_wallet = make_paying_wallet(b"consuming wallet"); - let (_, _, all_non_pending_payables) = - make_payables(SystemTime::now(), &PaymentThresholds::default()); + let (_, _, all_non_pending_payables) = make_qualified_and_unqualified_payables( + SystemTime::now(), + &PaymentThresholds::default(), + ); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); let mut subject = make_dull_subject(); @@ -1907,7 +1803,7 @@ mod tests { let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); let (_, unqualified_payable_accounts, _) = - make_payables(now, &PaymentThresholds::default()); + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); let payable_dao = PayableDaoMock::new().non_pending_payables_result(unqualified_payable_accounts); let mut subject = PayableScannerBuilder::new() @@ -1931,7 +1827,7 @@ mod tests { let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); let (qualified_payable_accounts, _, all_non_pending_payables) = - make_payables(now, &PaymentThresholds::default()); + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); let mut subject = make_dull_subject(); @@ -1971,8 +1867,10 @@ mod tests { #[test] fn retry_payable_scanner_panics_in_case_scan_is_already_running() { let consuming_wallet = make_paying_wallet(b"consuming wallet"); - let (_, _, all_non_pending_payables) = - make_payables(SystemTime::now(), &PaymentThresholds::default()); + let (_, _, all_non_pending_payables) = make_qualified_and_unqualified_payables( + SystemTime::now(), + &PaymentThresholds::default(), + ); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); let mut subject = make_dull_subject(); @@ -2053,7 +1951,7 @@ mod tests { let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); let (_, unqualified_payable_accounts, _) = - make_payables(now, &PaymentThresholds::default()); + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); let payable_dao = PayableDaoMock::new().non_pending_payables_result(unqualified_payable_accounts); let mut subject = PayableScannerBuilder::new() @@ -2127,10 +2025,11 @@ mod tests { }; subject.mark_as_started(SystemTime::now()); - let message_opt = subject.finish_scan(sent_payable, &logger); + let result = subject.finish_scan(sent_payable, &logger); let is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(message_opt, None); + let ui_msg_opt = result.finished(); + assert_eq!(ui_msg_opt, None); assert_eq!(is_scan_running, false); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); assert_eq!( @@ -2515,7 +2414,8 @@ mod tests { System::current().stop(); system.run(); - assert_eq!(result, None); + let ui_msg_opt = result.finished(); + assert_eq!(ui_msg_opt, None); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); assert_eq!( *fingerprints_rowids_params, @@ -3708,10 +3608,11 @@ mod tests { }; subject.mark_as_started(SystemTime::now()); - let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let result = subject.finish_scan(msg, &Logger::new(test_name)); let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!(message_opt, None); + let ui_msg_opt = result.finished(); + assert_eq!(ui_msg_opt, None); assert_eq!( *transactions_confirmed_params, vec![vec![fingerprint_1, fingerprint_2]] @@ -3738,10 +3639,11 @@ mod tests { }; subject.mark_as_started(SystemTime::now()); - let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let result = subject.finish_scan(msg, &Logger::new(test_name)); let is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(message_opt, None); + let ui_msg_opt = result.finished(); + assert_eq!(ui_msg_opt, None); assert_eq!(is_scan_running, false); let tlh = TestLogHandler::new(); tlh.exists_log_containing(&format!( @@ -3915,9 +3817,10 @@ mod tests { transactions: vec![], }; - let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let result = subject.finish_scan(msg, &Logger::new(test_name)); - assert_eq!(message_opt, None); + let ui_msg_opt = result.finished(); + assert_eq!(ui_msg_opt, None); let set_start_block_params = set_start_block_params_arc.lock().unwrap(); assert_eq!(*set_start_block_params, vec![Some(4321)]); TestLogHandler::new().exists_log_containing(&format!( @@ -4004,13 +3907,14 @@ mod tests { }; subject.mark_as_started(SystemTime::now()); - let message_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let result = subject.finish_scan(msg, &Logger::new(test_name)); let total_paid_receivable = subject .financial_statistics .borrow() .total_paid_receivable_wei; - assert_eq!(message_opt, None); + let ui_msg_opt = result.finished(); + assert_eq!(ui_msg_opt, None); assert_eq!(subject.scan_started_at(), None); assert_eq!(total_paid_receivable, 2_222_123_123 + 45_780 + 3_333_345); let more_money_received_params = more_money_received_params_arc.lock().unwrap(); @@ -4262,46 +4166,6 @@ mod tests { ); } - #[test] - fn scan_schedulers_can_be_properly_initialized() { - let scan_intervals = ScanIntervals { - payable_scan_interval: Duration::from_secs(240), - receivable_scan_interval: Duration::from_secs(360), - }; - - let result = ScanSchedulers::new(scan_intervals); - - assert_eq!( - result - .schedulers - .get(&ScanType::PendingPayables) - .unwrap() - .as_any() - .downcast_ref::>() - .unwrap() - .interval, - scan_intervals.payable_scan_interval - ); - result - .schedulers - .get(&ScanType::Payables) - .unwrap() - .as_any() - .downcast_ref::>() - .unwrap(); - assert_eq!( - result - .schedulers - .get(&ScanType::Receivables) - .unwrap() - .as_any() - .downcast_ref::>() - .unwrap() - .interval, - scan_intervals.receivable_scan_interval - ); - } - fn make_dull_subject() -> Scanners { Scanners { payable: Box::new(NullScanner::new()), diff --git a/node/src/accountant/scanners/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner/mod.rs index b2d3d61da..bc4f525c6 100644 --- a/node/src/accountant/scanners/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner/mod.rs @@ -6,18 +6,14 @@ pub mod blockchain_agent; pub mod msgs; pub mod test_utils; -use crate::accountant::db_access_objects::payable_dao::PayableDao; -use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDao; -use crate::accountant::payment_adjuster::{Adjustment, PaymentAdjuster}; +use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::{AccessibleScanner, InaccessibleScanner, ScanWithStarter}; +use crate::accountant::scanners::{AccessibleScanner, ScanWithStarter}; use crate::accountant::{ScanForNewPayables, ScanForRetryPayables}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use actix::Message; -use itertools::{Either, Itertools}; +use itertools::Either; use masq_lib::logger::Logger; -use masq_lib::messages::ToMessageBody; -use masq_lib::utils::ExpectValue; pub trait MultistageDualPayableScanner: ScanWithStarter diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs new file mode 100644 index 000000000..149ec5dfe --- /dev/null +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -0,0 +1,303 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::{ + Accountant, ResponseSkeleton, ScanForNewPayables, ScanForPendingPayables, ScanForReceivables, + ScanForRetryPayables, +}; +use crate::sub_lib::accountant::ScanIntervals; +use crate::sub_lib::utils::{ + NotifyHandle, NotifyHandleReal, NotifyLaterHandle, NotifyLaterHandleReal, +}; +use actix::{Context, Handler}; +use std::cell::RefCell; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +pub struct ScanSchedulers { + pub payable: PayableScanScheduler, + pub pending_payable: SimplePeriodicalScanScheduler, + pub receivable: SimplePeriodicalScanScheduler, +} + +impl ScanSchedulers { + pub fn new(scan_intervals: ScanIntervals) -> Self { + Self { + payable: PayableScanScheduler::new(scan_intervals.payable_scan_interval), + pending_payable: SimplePeriodicalScanScheduler::new( + scan_intervals.pending_payable_scan_interval, + ), + receivable: SimplePeriodicalScanScheduler::new(scan_intervals.receivable_scan_interval), + } + } +} + +pub struct PayableScanScheduler { + pub new_payable_notify_later: Box>, + pub dyn_interval_computer: Box, + pub last_new_payable_scan_timestamp: RefCell, + pub nominal_interval: Duration, + pub new_payable_notify: Box>, + pub retry_payable_notify: Box>, +} + +impl PayableScanScheduler { + fn new(nominal_interval: Duration) -> Self { + Self { + new_payable_notify_later: Box::new(NotifyLaterHandleReal::default()), + dyn_interval_computer: Box::new(NewPayableScanDynIntervalComputerReal::default()), + last_new_payable_scan_timestamp: RefCell::new(UNIX_EPOCH), + nominal_interval, + new_payable_notify: Box::new(NotifyHandleReal::default()), + retry_payable_notify: Box::new(NotifyHandleReal::default()), + } + } + + pub fn schedule_for_new_payable(&self, ctx: &mut Context) { + let last_new_payable_timestamp = *self.last_new_payable_scan_timestamp.borrow(); + let nominal_interval = self.nominal_interval; + let now = SystemTime::now(); + if let Some(interval) = self.dyn_interval_computer.compute_interval( + now, + last_new_payable_timestamp, + nominal_interval, + ) { + let _ = self.new_payable_notify_later.notify_later( + ScanForNewPayables { + response_skeleton_opt: None, + }, + interval, + ctx, + ); + } else { + let _ = self.new_payable_notify.notify( + ScanForNewPayables { + response_skeleton_opt: None, + }, + ctx, + ); + } + } + + // Can be triggered by a command, whereas the finished pending payable scanner is followed up + // by this scheduled message. It is inserted into the Accountant's mailbox right away (no + // interval) + pub fn schedule_for_retry_payable( + &self, + ctx: &mut Context, + response_skeleton_opt: Option, + ) { + self.retry_payable_notify.notify( + ScanForRetryPayables { + response_skeleton_opt, + }, + ctx, + ) + } +} + +pub trait NewPayableScanDynIntervalComputer { + fn compute_interval( + &self, + now: SystemTime, + last_new_payable_scan_timestamp: SystemTime, + interval: Duration, + ) -> Option; +} + +#[derive(Default)] +pub struct NewPayableScanDynIntervalComputerReal {} + +impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerReal { + fn compute_interval( + &self, + now: SystemTime, + last_new_payable_scan_timestamp: SystemTime, + interval: Duration, + ) -> Option { + let elapsed = now + .duration_since(last_new_payable_scan_timestamp) + .unwrap_or_else(|_| { + panic!( + "Unexpected now ({:?}) earlier than past timestamp ({:?})", + now, last_new_payable_scan_timestamp + ) + }); + if elapsed >= interval { + None + } else { + Some(interval - elapsed) + } + } +} + +pub struct SimplePeriodicalScanScheduler { + pub handle: Box>, + pub interval: Duration, +} + +impl SimplePeriodicalScanScheduler +where + Message: actix::Message + Default + 'static, + Accountant: Handler, +{ + fn new(interval: Duration) -> Self { + Self { + handle: Box::new(NotifyLaterHandleReal::default()), + interval, + } + } + pub fn schedule( + &self, + ctx: &mut Context, + _response_skeleton_opt: Option, + ) { + // the default of the message implies response_skeleton_opt to be None + // because scheduled scans don't respond + let _ = self + .handle + .notify_later(Message::default(), self.interval, ctx); + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::scanners::scan_schedulers::{ + NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, ScanSchedulers, + }; + use crate::sub_lib::accountant::ScanIntervals; + use std::time::{Duration, SystemTime, UNIX_EPOCH}; + + #[test] + fn scan_schedulers_are_initialized_correctly() { + let scan_intervals = ScanIntervals { + payable_scan_interval: Duration::from_secs(14), + pending_payable_scan_interval: Duration::from_secs(2), + receivable_scan_interval: Duration::from_secs(7), + }; + + let result = ScanSchedulers::new(scan_intervals); + + assert_eq!( + result.payable.nominal_interval, + scan_intervals.payable_scan_interval + ); + assert_eq!( + *result.payable.last_new_payable_scan_timestamp.borrow(), + UNIX_EPOCH + ); + assert_eq!( + result.pending_payable.interval, + scan_intervals.pending_payable_scan_interval + ); + assert_eq!( + result.receivable.interval, + scan_intervals.receivable_scan_interval + ) + } + + #[test] + fn scan_dyn_interval_computer_computes_remaining_time_to_standard_interval_correctly() { + let now = SystemTime::now(); + let inputs = vec![ + ( + now.checked_sub(Duration::from_secs(32)).unwrap(), + Duration::from_secs(100), + Duration::from_secs(68), + ), + ( + now.checked_sub(Duration::from_millis(1111)).unwrap(), + Duration::from_millis(3333), + Duration::from_millis(2222), + ), + ( + now.checked_sub(Duration::from_secs(200)).unwrap(), + Duration::from_secs(204), + Duration::from_secs(4), + ), + ]; + let subject = NewPayableScanDynIntervalComputerReal::default(); + + inputs + .into_iter() + .for_each(|(past_instant, standard_interval, expected_result)| { + let result = subject.compute_interval(now, past_instant, standard_interval); + assert_eq!( + result, + Some(expected_result), + "We expected Some({}) ms, but got {:?} ms", + expected_result.as_millis(), + result.map(|duration| duration.as_millis()) + ) + }) + } + + #[test] + fn scan_dyn_interval_computer_realizes_the_standard_interval_has_been_exceeded() { + let now = SystemTime::now(); + let inputs = vec![ + ( + now.checked_sub(Duration::from_millis(32001)).unwrap(), + Duration::from_secs(32), + ), + ( + now.checked_sub(Duration::from_millis(1112)).unwrap(), + Duration::from_millis(1111), + ), + ( + now.checked_sub(Duration::from_secs(200)).unwrap(), + Duration::from_secs(123), + ), + ]; + let subject = NewPayableScanDynIntervalComputerReal::default(); + + inputs + .into_iter() + .enumerate() + .for_each(|(idx, (past_instant, standard_interval))| { + let result = subject.compute_interval(now, past_instant, standard_interval); + assert_eq!( + result, + None, + "We expected None ms, but got {:?} ms at idx {}", + result.map(|duration| duration.as_millis()), + idx + ) + }) + } + + #[test] + fn scan_dyn_interval_computer_realizes_standard_interval_just_met() { + let now = SystemTime::now(); + let subject = NewPayableScanDynIntervalComputerReal::default(); + + let result = subject.compute_interval( + now, + now.checked_sub(Duration::from_secs(32)).unwrap(), + Duration::from_secs(32), + ); + + assert_eq!( + result, + None, + "We expected None ms, but got {:?} ms", + result.map(|duration| duration.as_millis()) + ) + } + + #[test] + #[should_panic( + expected = "Unexpected now (SystemTime { tv_sec: 999999, tv_nsec: 0 }) earlier \ + than past timestamp (SystemTime { tv_sec: 1000000, tv_nsec: 0 })" + )] + fn scan_dyn_interval_computer_panics() { + let now = UNIX_EPOCH + .checked_add(Duration::from_secs(1_000_000)) + .unwrap(); + let subject = NewPayableScanDynIntervalComputerReal::default(); + + let _ = subject.compute_interval( + now.checked_sub(Duration::from_secs(1)).unwrap(), + now, + Duration::from_secs(32), + ); + } +} diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 30b3a3d2d..26707c9be 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -321,6 +321,12 @@ pub mod pending_payable_scanner_utils { pub confirmed: Vec, } + impl PendingPayableScanReport { + pub fn requires_payments_retry(&self) -> bool { + !self.still_pending.is_empty() || !self.failures.is_empty() + } + } + pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { timestamp .elapsed() @@ -452,7 +458,7 @@ mod tests { PayableThresholdsGaugeReal, }; use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; - use crate::accountant::{checked_conversion, gwei_to_wei, SentPayables}; + use crate::accountant::{checked_conversion, gwei_to_wei, PendingPayableId, SentPayables}; use crate::blockchain::test_utils::make_tx_hash; use crate::sub_lib::accountant::PaymentThresholds; use crate::test_utils::make_wallet; @@ -461,6 +467,8 @@ mod tests { use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use std::time::SystemTime; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; + use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanReport; + use crate::accountant::test_utils::make_pending_payable_fingerprint; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RpcPayableFailure}; @@ -843,4 +851,62 @@ mod tests { "Got 0 properly sent payables of an unknown number of attempts" ) } + + #[test] + fn requires_payments_retry_says_yes() { + let cases = vec![ + PendingPayableScanReport { + still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + failures: vec![], + confirmed: vec![], + }, + PendingPayableScanReport { + still_pending: vec![], + failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + confirmed: vec![], + }, + PendingPayableScanReport { + still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + confirmed: vec![], + }, + PendingPayableScanReport { + still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + confirmed: vec![make_pending_payable_fingerprint()], + }, + PendingPayableScanReport { + still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + failures: vec![], + confirmed: vec![make_pending_payable_fingerprint()], + }, + PendingPayableScanReport { + still_pending: vec![], + failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + confirmed: vec![make_pending_payable_fingerprint()], + }, + ]; + + cases.into_iter().enumerate().for_each(|(idx, case)| { + let result = case.requires_payments_retry(); + assert_eq!( + result, true, + "We expected true, but got false for case of idx {}", + idx + ) + }) + } + + #[test] + fn requires_payments_retry_says_no() { + let report = PendingPayableScanReport { + still_pending: vec![], + failures: vec![], + confirmed: vec![make_pending_payable_fingerprint()], + }; + + let result = report.requires_payments_retry(); + + assert_eq!(result, false) + } } diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index c43d6f71b..87f8c8636 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -3,8 +3,49 @@ #![cfg(test)] use crate::accountant::db_access_objects::payable_dao::PayableAccount; +use crate::accountant::scanners::scan_schedulers::NewPayableScanDynIntervalComputer; use masq_lib::type_obfuscation::Obfuscated; +use std::cell::RefCell; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime}; pub fn protect_payables_in_test(payables: Vec) -> Obfuscated { Obfuscated::obfuscate_vector(payables) } + +#[derive(Default)] +pub struct NewPayableScanDynIntervalComputerMock { + compute_interval_params: Arc>>, + compute_interval_results: RefCell>>, +} + +impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerMock { + fn compute_interval( + &self, + now: SystemTime, + last_new_payable_scan_timestamp: SystemTime, + interval: Duration, + ) -> Option { + self.compute_interval_params.lock().unwrap().push(( + now, + last_new_payable_scan_timestamp, + interval, + )); + self.compute_interval_results.borrow_mut().remove(0) + } +} + +impl NewPayableScanDynIntervalComputerMock { + pub fn compute_interval_params( + mut self, + params: &Arc>>, + ) -> Self { + self.compute_interval_params = params.clone(); + self + } + + pub fn compute_interval_results(self, result: Option) -> Self { + self.compute_interval_results.borrow_mut().push(result); + self + } +} diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index a68a1b7cd..160aff952 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -1245,7 +1245,7 @@ pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { } } -pub fn make_payables( +pub fn make_qualified_and_unqualified_payables( now: SystemTime, payment_thresholds: &PaymentThresholds, ) -> ( diff --git a/node/src/db_config/persistent_configuration.rs b/node/src/db_config/persistent_configuration.rs index ed02642f6..ebc4efba6 100644 --- a/node/src/db_config/persistent_configuration.rs +++ b/node/src/db_config/persistent_configuration.rs @@ -1949,9 +1949,10 @@ mod tests { fn scan_intervals_get_method_works() { persistent_config_plain_data_assertions_for_simple_get_method!( "scan_intervals", - "60|50", + "60|5|50", ScanIntervals { payable_scan_interval: Duration::from_secs(60), + pending_payable_scan_interval: Duration::from_secs(5), receivable_scan_interval: Duration::from_secs(50), } ); diff --git a/node/src/node_configurator/configurator.rs b/node/src/node_configurator/configurator.rs index 750da542f..fdf0199cd 100644 --- a/node/src/node_configurator/configurator.rs +++ b/node/src/node_configurator/configurator.rs @@ -2608,6 +2608,7 @@ mod tests { })) .scan_intervals_result(Ok(ScanIntervals { payable_scan_interval: Duration::from_secs(125), + pending_payable_scan_interval: Duration::from_secs(30), receivable_scan_interval: Duration::from_secs(128), })) .payment_thresholds_result(Ok(PaymentThresholds { @@ -2756,6 +2757,7 @@ mod tests { })) .scan_intervals_result(Ok(ScanIntervals { payable_scan_interval: Default::default(), + pending_payable_scan_interval: Default::default(), receivable_scan_interval: Default::default(), })) .payment_thresholds_result(Ok(PaymentThresholds { diff --git a/node/src/node_configurator/unprivileged_parse_args_configuration.rs b/node/src/node_configurator/unprivileged_parse_args_configuration.rs index 38d643864..33b38fa5a 100644 --- a/node/src/node_configurator/unprivileged_parse_args_configuration.rs +++ b/node/src/node_configurator/unprivileged_parse_args_configuration.rs @@ -1818,7 +1818,7 @@ mod tests { "--ip", "1.2.3.4", "--scan-intervals", - "180|150|130", + "180|50|130", "--payment-thresholds", "100000|10000|1000|20000|1000|20000", ]; @@ -1828,6 +1828,7 @@ mod tests { configure_default_persistent_config(RATE_PACK | MAPPING_PROTOCOL) .scan_intervals_result(Ok(ScanIntervals { payable_scan_interval: Duration::from_secs(101), + pending_payable_scan_interval: Duration::from_secs(33), receivable_scan_interval: Duration::from_secs(102), })) .payment_thresholds_result(Ok(PaymentThresholds { @@ -1854,7 +1855,8 @@ mod tests { .unwrap(); let expected_scan_intervals = ScanIntervals { - payable_scan_interval: Duration::from_secs(150), + payable_scan_interval: Duration::from_secs(180), + pending_payable_scan_interval: Duration::from_secs(50), receivable_scan_interval: Duration::from_secs(130), }; let expected_payment_thresholds = PaymentThresholds { @@ -1876,7 +1878,7 @@ mod tests { DEFAULT_PENDING_TOO_LONG_SEC ); let set_scan_intervals_params = set_scan_intervals_params_arc.lock().unwrap(); - assert_eq!(*set_scan_intervals_params, vec!["180|150|130".to_string()]); + assert_eq!(*set_scan_intervals_params, vec!["180|50|130".to_string()]); let set_payment_thresholds_params = set_payment_thresholds_params_arc.lock().unwrap(); assert_eq!( *set_payment_thresholds_params, @@ -1892,7 +1894,7 @@ mod tests { "--ip", "1.2.3.4", "--scan-intervals", - "180|150|130", + "180|15|130", "--payment-thresholds", "100000|1000|1000|20000|1000|20000", ]; @@ -1901,7 +1903,8 @@ mod tests { let mut persistent_configuration = configure_default_persistent_config(RATE_PACK | MAPPING_PROTOCOL) .scan_intervals_result(Ok(ScanIntervals { - payable_scan_interval: Duration::from_secs(150), + payable_scan_interval: Duration::from_secs(180), + pending_payable_scan_interval: Duration::from_secs(15), receivable_scan_interval: Duration::from_secs(130), })) .payment_thresholds_result(Ok(PaymentThresholds { @@ -1932,7 +1935,8 @@ mod tests { unban_below_gwei: 20000, }; let expected_scan_intervals = ScanIntervals { - payable_scan_interval: Duration::from_secs(150), + payable_scan_interval: Duration::from_secs(180), + pending_payable_scan_interval: Duration::from_secs(15), receivable_scan_interval: Duration::from_secs(130), }; let expected_suppress_initial_scans = false; diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index f4b893db9..878484a25 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -39,6 +39,7 @@ lazy_static! { }; pub static ref DEFAULT_SCAN_INTERVALS: ScanIntervals = ScanIntervals { payable_scan_interval: Duration::from_secs(600), + pending_payable_scan_interval: Duration::from_secs(60), receivable_scan_interval: Duration::from_secs(600) }; } @@ -79,6 +80,7 @@ pub struct DaoFactories { #[derive(PartialEq, Eq, Debug, Clone, Copy)] pub struct ScanIntervals { pub payable_scan_interval: Duration, + pub pending_payable_scan_interval: Duration, pub receivable_scan_interval: Duration, } @@ -230,6 +232,7 @@ mod tests { }; let scan_intervals_expected = ScanIntervals { payable_scan_interval: Duration::from_secs(600), + pending_payable_scan_interval: Duration::from_secs(60), receivable_scan_interval: Duration::from_secs(600), }; assert_eq!(*DEFAULT_SCAN_INTERVALS, scan_intervals_expected); diff --git a/node/src/sub_lib/combined_parameters.rs b/node/src/sub_lib/combined_parameters.rs index 56a2ab013..e76d7cf78 100644 --- a/node/src/sub_lib/combined_parameters.rs +++ b/node/src/sub_lib/combined_parameters.rs @@ -178,6 +178,7 @@ impl CombinedParams { &parsed_values, Duration::from_secs, "payable_scan_interval", + "pending_payable_scan_interval", "receivable_scan_interval" ))) } @@ -207,8 +208,8 @@ impl From<&CombinedParams> for &[(&str, CombinedParamsDataTypes)] { ("unban_below_gwei", U64), ], CombinedParams::ScanIntervals(Uninitialized) => &[ - ("pending_payable_scan_interval", U64), ("payable_scan_interval", U64), + ("pending_payable_scan_interval", U64), ("receivable_scan_interval", U64), ], _ => panic!( @@ -548,7 +549,7 @@ mod tests { #[test] fn scan_intervals_from_combined_params() { - let scan_intervals_str = "115|113"; + let scan_intervals_str = "115|55|113"; let result = ScanIntervals::try_from(scan_intervals_str).unwrap(); @@ -556,6 +557,7 @@ mod tests { result, ScanIntervals { payable_scan_interval: Duration::from_secs(115), + pending_payable_scan_interval: Duration::from_secs(55), receivable_scan_interval: Duration::from_secs(113) } ) @@ -564,13 +566,14 @@ mod tests { #[test] fn scan_intervals_to_combined_params() { let scan_intervals = ScanIntervals { - payable_scan_interval: Duration::from_secs(70), + payable_scan_interval: Duration::from_secs(90), + pending_payable_scan_interval: Duration::from_secs(40), receivable_scan_interval: Duration::from_secs(100), }; let result = scan_intervals.to_string(); - assert_eq!(result, "70|100".to_string()); + assert_eq!(result, "90|40|100".to_string()); } #[test] diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 4112186b6..7d0c0df73 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -874,6 +874,7 @@ pub mod unshared_test_utils { pub struct NotifyLaterHandleMock { notify_later_params: Arc>>, + stop_system_after_call_num_opt: RefCell>, send_message_out: bool, } @@ -881,6 +882,7 @@ pub mod unshared_test_utils { fn default() -> Self { Self { notify_later_params: Arc::new(Mutex::new(vec![])), + stop_system_after_call_num_opt: RefCell::new(None), send_message_out: false, } } @@ -892,10 +894,40 @@ pub mod unshared_test_utils { self } + pub fn stop_system_after_call_count(self, count: usize) -> Self { + if count == 0 { + panic!("Should be considered starting from 1") + } + self.stop_system_after_call_num_opt.replace(Some(count)); + self + } + pub fn capture_msg_and_let_it_fly_on(mut self) -> Self { self.send_message_out = true; self } + + fn should_stop_the_system(&self) { + let stop = if let Some(allowed_calls) = + self.stop_system_after_call_num_opt.borrow().as_ref() + { + *allowed_calls == 1 + } else { + return; + }; + if stop { + System::current().stop() + } else { + let allowed_calls = *self + .stop_system_after_call_num_opt + .borrow() + .as_ref() + .unwrap(); + let _ = self + .stop_system_after_call_num_opt + .replace(Some(allowed_calls - 1)); + } + } } impl NotifyLaterHandle for NotifyLaterHandleMock @@ -913,6 +945,7 @@ pub mod unshared_test_utils { .lock() .unwrap() .push((msg.clone(), interval)); + self.should_stop_the_system(); if self.send_message_out { let handle = ctx.notify_later(msg, interval); Box::new(NLSpawnHandleHolderReal::new(handle)) From e1a0a596b7f0fc79c31e10742c182868a3f7761d Mon Sep 17 00:00:00 2001 From: Bert Date: Sat, 19 Apr 2025 23:29:03 +0200 Subject: [PATCH 05/49] GH-602: interim commit --- node/src/accountant/mod.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 152f82e58..7dd475688 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1199,7 +1199,7 @@ mod tests { use masq_lib::ui_gateway::MessagePath::Conversation; use masq_lib::ui_gateway::{MessageBody, MessagePath, NodeFromUiMessage, NodeToUiMessage}; use std::any::TypeId; - use std::ops::{Add, Sub}; + use std::ops::{Sub}; use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; @@ -3648,11 +3648,15 @@ mod tests { system.run(); let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); assert_eq!(*transactions_confirmed_params, vec![two_fingerprints]); - let compute_interval_params = compute_interval_params_arc.lock().unwrap(); + let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); + let (_, last_new_payable_timestamp_actual, scan_interval_actual) = + compute_interval_params.remove(0); assert_eq!( - *compute_interval_params, - vec![(last_new_payable_scan_timestamp, nominal_interval)] + last_new_payable_timestamp_actual, + last_new_payable_scan_timestamp ); + assert_eq!(scan_interval_actual, nominal_interval); + assert!(compute_interval_params.is_empty()); let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); assert_eq!( *new_payable_notify_later, @@ -3719,11 +3723,15 @@ mod tests { system.run(); let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); assert_eq!(*transactions_confirmed_params, vec![two_fingerprints]); - let compute_interval_params = compute_interval_params_arc.lock().unwrap(); + let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); + let (_, last_new_payable_timestamp_actual, scan_interval_actual) = + compute_interval_params.remove(0); assert_eq!( - *compute_interval_params, - vec![(last_new_payable_scan_timestamp, nominal_interval)] + last_new_payable_timestamp_actual, + last_new_payable_scan_timestamp ); + assert_eq!(scan_interval_actual, nominal_interval); + assert!(compute_interval_params.is_empty()); let new_payable_notify_later = new_payable_notify_later_arc.lock().unwrap(); assert!( new_payable_notify_later.is_empty(), From f83ece29ebe1863be263a50a89f32d740c72da8a Mon Sep 17 00:00:00 2001 From: Bert Date: Wed, 23 Apr 2025 10:52:00 +0200 Subject: [PATCH 06/49] GH-602: Recorder enriched by counter messages --- node/src/accountant/mod.rs | 2 +- node/src/blockchain/blockchain_bridge.rs | 2 +- node/src/proxy_server/mod.rs | 2 +- node/src/test_utils/mod.rs | 7 +- node/src/test_utils/recorder.rs | 234 +++++++++++++++++- node/src/test_utils/recorder_counter_msgs.rs | 115 +++++++++ .../test_utils/recorder_stop_conditions.rs | 88 +++---- 7 files changed, 397 insertions(+), 53 deletions(-) create mode 100644 node/src/test_utils/recorder_counter_msgs.rs diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 7dd475688..39c678862 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1171,7 +1171,7 @@ mod tests { use crate::test_utils::recorder::make_recorder; use crate::test_utils::recorder::peer_actors_builder; use crate::test_utils::recorder::Recorder; - use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; + use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; use crate::test_utils::unshared_test_utils::notify_handlers::{NotifyHandleMock, NotifyLaterHandleMock}; use crate::test_utils::unshared_test_utils::system_killer_actor::SystemKillerActor; diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 0b940e271..fdf636efd 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -573,7 +573,7 @@ mod tests { use crate::test_utils::recorder::{ make_accountant_subs_from_recorder, make_recorder, peer_actors_builder, }; - use crate::test_utils::recorder_stop_conditions::StopCondition; + use crate::test_utils::recorder_stop_conditions::MsgIdentification; use crate::test_utils::recorder_stop_conditions::StopConditions; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; use crate::test_utils::unshared_test_utils::{ diff --git a/node/src/proxy_server/mod.rs b/node/src/proxy_server/mod.rs index 5ea296a4b..3cf09abed 100644 --- a/node/src/proxy_server/mod.rs +++ b/node/src/proxy_server/mod.rs @@ -1375,7 +1375,7 @@ mod tests { use crate::test_utils::recorder::make_recorder; use crate::test_utils::recorder::peer_actors_builder; use crate::test_utils::recorder::Recorder; - use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; + use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; use crate::test_utils::unshared_test_utils::{ make_request_payload, prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, }; diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 7d0c0df73..e2fd38419 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -12,6 +12,7 @@ pub mod logfile_name_guard; pub mod neighborhood_test_utils; pub mod persistent_configuration_mock; pub mod recorder; +mod recorder_counter_msgs; pub mod recorder_stop_conditions; pub mod stream_connector_mock; pub mod tcp_wrapper_mocks; @@ -539,7 +540,7 @@ pub mod unshared_test_utils { use crate::test_utils::neighborhood_test_utils::MIN_HOPS_FOR_TEST; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::recorder::{make_recorder, Recorder, Recording}; - use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; + use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; use crate::test_utils::unshared_test_utils::system_killer_actor::SystemKillerActor; use actix::{Actor, Addr, AsyncContext, Context, Handler, Recipient, System}; use actix::{Message, SpawnHandle}; @@ -699,8 +700,8 @@ pub mod unshared_test_utils { let (recorder, _, recording_arc) = make_recorder(); let recorder = match stopping_message { Some(type_id) => recorder.system_stop_conditions(StopConditions::All(vec![ - StopCondition::StopOnType(type_id), - ])), // No need to write stop message after this + MsgIdentification::ByType(type_id), + ])), // This will take care of stopping the system None => recorder, }; let addr = recorder.start(); diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index 3e10a4d70..64edc59ba 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -47,8 +47,9 @@ use crate::sub_lib::stream_handler_pool::DispatcherNodeQueryResponse; use crate::sub_lib::stream_handler_pool::TransmitDataMsg; use crate::sub_lib::ui_gateway::UiGatewaySubs; use crate::sub_lib::utils::MessageScheduler; +use crate::test_utils::recorder_counter_msgs::{CounterMessages, CounterMsgGear, CounterMsgSetup}; use crate::test_utils::recorder_stop_conditions::{ - ForcedMatchable, PretendedMatchableWrapper, StopCondition, StopConditions, + ForcedMatchable, MsgIdentification, PretendedMatchableWrapper, StopConditions, }; use crate::test_utils::to_millis; use crate::test_utils::unshared_test_utils::system_killer_actor::SystemKillerActor; @@ -70,6 +71,7 @@ pub struct Recorder { recording: Arc>, node_query_responses: Vec>, route_query_responses: Vec>, + expected_future_counter_msgs_opt: Option, stop_conditions_opt: Option, } @@ -260,11 +262,22 @@ impl Recorder { self.start_system_killer(); self.stop_conditions_opt = Some(stop_conditions) } else { - panic!("Stop conditions must be set by a single method call. Consider to use StopConditions::All") + panic!("Stop conditions must be set by a single method call. Consider using StopConditions::All") }; self } + pub fn add_counter_msg(mut self, counter_msg_setup: CounterMsgSetup) -> Self { + if let Some(counter_msgs) = self.expected_future_counter_msgs_opt.as_mut() { + counter_msgs.add_msg(counter_msg_setup) + } else { + let mut counter_msgs = CounterMessages::default(); + counter_msgs.add_msg(counter_msg_setup); + self.expected_future_counter_msgs_opt = Some(counter_msgs) + } + self + } + fn start_system_killer(&mut self) { let system_killer = SystemKillerActor::new(Duration::from_secs(15)); system_killer.start(); @@ -274,6 +287,8 @@ impl Recorder { where M: 'static + ForcedMatchable + Send, { + let counter_msg_opt = self.check_on_counter_msg(&msg); + let kill_system = if let Some(stop_conditions) = &mut self.stop_conditions_opt { stop_conditions.resolve_stop_conditions::(&msg) } else { @@ -282,6 +297,10 @@ impl Recorder { self.record(msg); + if let Some(sendable_msg) = counter_msg_opt { + sendable_msg.try_send() + } + if kill_system { System::current().stop() } @@ -294,6 +313,17 @@ impl Recorder { { self.handle_msg_t_m_p(PretendedMatchableWrapper(msg)) } + + fn check_on_counter_msg(&mut self, msg: &M) -> Option> + where + M: ForcedMatchable + 'static, + { + if let Some(counter_msgs) = self.expected_future_counter_msgs_opt.as_mut() { + counter_msgs.search_for_msg_setup(msg) + } else { + None + } + } } impl Recording { @@ -343,7 +373,7 @@ impl Recording { match item_box.downcast_ref::() { Some(item) => Ok(item), None => { - // double-checking for an uncommon, yet possible other type of an actor message, which doesn't implement PartialEq + // double-checking for an uncommon, yet possible other type of actor message, which doesn't implement PartialEq let item_opt = item_box.downcast_ref::>(); match item_opt { @@ -604,10 +634,19 @@ impl PeerActorsBuilder { #[cfg(test)] mod tests { use super::*; + use crate::blockchain::blockchain_bridge::BlockchainBridge; use crate::match_every_type_id; + use crate::sub_lib::neighborhood::{ConfigChange, Hops, WalletPair}; + use crate::test_utils::make_wallet; + use crate::test_utils::neighborhood_test_utils::make_ip; use actix::Message; use actix::System; + use masq_lib::messages::{ + SerializableLogLevel, ToMessageBody, UiLogBroadcast, UiUnmarshalError, + }; + use masq_lib::ui_gateway::MessageTarget; use std::any::TypeId; + use std::net::{IpAddr, Ipv4Addr}; #[derive(Debug, PartialEq, Eq, Message)] struct FirstMessageType { @@ -704,4 +743,193 @@ mod tests { TypeId::of::>() ) } + + #[test] + fn diff_counter_msgs_with_diff_id_methods_can_be_used() { + let (respondent, _, respondent_recording_arc) = make_recorder(); + let respondent = respondent + .system_stop_conditions(match_every_type_id!(ScanForReceivables, NodeToUiMessage)); + let respondent_addr = respondent.start(); + let msg_1 = StartMessage {}; + let msg_1_type_id = msg_1.type_id(); + let counter_msg_1 = ScanForReceivables { + response_skeleton_opt: None, + }; + let id_method_1 = MsgIdentification::ByType(msg_1.type_id()); + let setup_1 = + CounterMsgSetup::new(msg_1_type_id, id_method_1, counter_msg_1, &respondent_addr); + let counter_msg_2_strayed = StartMessage {}; + let random_id = TypeId::of::(); + let id_method_2 = MsgIdentification::ByType(random_id); + let setup_2 = CounterMsgSetup::new( + random_id, + id_method_2, + counter_msg_2_strayed, + &respondent_addr, + ); + let msg_3_unmatching = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(7, 7, 7, 7)), + }; + let msg_3_type_id = msg_3_unmatching.type_id(); + let counter_msg_3 = NodeToUiMessage { + target: MessageTarget::ClientId(4), + body: UiUnmarshalError { + message: "abc".to_string(), + bad_data: "456".to_string(), + } + .tmb(0), + }; + let id_method_3 = MsgIdentification::ByMatch { + exemplar: Box::new(NewPublicIp { new_ip: make_ip(1) }), + }; + let setup_3 = + CounterMsgSetup::new(msg_3_type_id, id_method_3, counter_msg_3, &respondent_addr); + let msg_4_matching = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(4, 5, 6, 7)), + }; + let msg_4_type_id = msg_4_matching.type_id(); + let counter_msg_4 = NodeToUiMessage { + target: MessageTarget::ClientId(234), + body: UiLogBroadcast { + msg: "Good one".to_string(), + log_level: SerializableLogLevel::Error, + } + .tmb(0), + }; + let id_method_4 = MsgIdentification::ByMatch { + exemplar: Box::new(msg_4_matching.clone()), + }; + let setup_4 = CounterMsgSetup::new( + msg_4_type_id, + id_method_4, + counter_msg_4.clone(), + &respondent_addr, + ); + let system = System::new("test"); + let (subject, _, subject_recording_arc) = make_recorder(); + // Adding messages in a tangled manner + let subject = subject + .add_counter_msg(setup_3) + .add_counter_msg(setup_1) + .add_counter_msg(setup_2) + .add_counter_msg(setup_4); + let subject_addr = subject.start(); + + subject_addr.try_send(msg_1).unwrap(); + subject_addr.try_send(msg_3_unmatching.clone()).unwrap(); + subject_addr.try_send(msg_4_matching.clone()).unwrap(); + + system.run(); + let respondent_recording = respondent_recording_arc.lock().unwrap(); + let _first_counter_msg_recorded = respondent_recording.get_record::(0); + let second_counter_msg_recorded = respondent_recording.get_record::(1); + assert_eq!(second_counter_msg_recorded, &counter_msg_4); + assert_eq!(respondent_recording.len(), 2); + let subject_recording = subject_recording_arc.lock().unwrap(); + let _first_subject_recorded_msg = subject_recording.get_record::(0); + let second_subject_recorded_msg = subject_recording.get_record::(1); + assert_eq!(second_subject_recorded_msg, &msg_3_unmatching); + let third_subject_recording_msg = subject_recording.get_record::(2); + assert_eq!(third_subject_recording_msg, &msg_4_matching); + assert_eq!(subject_recording.len(), 3) + } + + #[test] + fn counter_msgs_of_same_type_are_revisited_sequentially_and_triggered_by_first_matching_id_method( + ) { + let (respondent, _, respondent_recording_arc) = make_recorder(); + let respondent = respondent.system_stop_conditions(match_every_type_id!( + ConfigChangeMsg, + ConfigChangeMsg, + ConfigChangeMsg + )); + let respondent_addr = respondent.start(); + let msg_1 = CrashNotification { + process_id: 7777777, + exit_code: None, + stderr: Some("blah".to_string()), + }; + let msg_1_type_id = msg_1.type_id(); + let counter_msg_1 = ConfigChangeMsg { + change: ConfigChange::UpdateMinHops(Hops::SixHops), + }; + let id_method_1 = MsgIdentification::ByPredicate { + predicate: Box::new(|msg_boxed| { + let msg = msg_boxed.downcast_ref::().unwrap(); + msg.process_id == 1010 + }), + }; + let setup_1 = + CounterMsgSetup::new(msg_1_type_id, id_method_1, counter_msg_1, &respondent_addr); + let msg_2 = CrashNotification { + process_id: 1010, + exit_code: Some(11), + stderr: None, + }; + let msg_2_type_id = msg_2.type_id(); + let counter_msg_2 = ConfigChangeMsg { + change: ConfigChange::UpdatePassword("betterPassword".to_string()), + }; + let id_method_2 = MsgIdentification::ByType(msg_2.type_id()); + let setup_2 = + CounterMsgSetup::new(msg_2_type_id, id_method_2, counter_msg_2, &respondent_addr); + let msg_3 = CrashNotification { + process_id: 9999999, + exit_code: None, + stderr: None, + }; + let msg_3_type_id = msg_3.type_id(); + let counter_msg_3 = ConfigChangeMsg { + change: ConfigChange::UpdateWallets(WalletPair { + consuming_wallet: make_wallet("abc"), + earning_wallet: make_wallet("def"), + }), + }; + let id_method_3 = MsgIdentification::ByType(msg_3.type_id()); + let setup_3 = + CounterMsgSetup::new(msg_3_type_id, id_method_3, counter_msg_3, &respondent_addr); + let system = System::new("test"); + let (subject, _, subject_recording_arc) = make_recorder(); + // Adding messages in standard order + let subject = subject + .add_counter_msg(setup_1) + .add_counter_msg(setup_2) + .add_counter_msg(setup_3); + let subject_addr = subject.start(); + + // More tricky scenario picked on purpose + subject_addr.try_send(msg_3.clone()).unwrap(); + subject_addr.try_send(msg_2.clone()).unwrap(); + subject_addr.try_send(msg_1.clone()).unwrap(); + + system.run(); + let respondent_recording = respondent_recording_arc.lock().unwrap(); + let first_counter_msg_recorded = respondent_recording.get_record::(0); + assert_eq!( + first_counter_msg_recorded.change, + ConfigChange::UpdatePassword("betterPassword".to_string()) + ); + let second_counter_msg_recorded = respondent_recording.get_record::(1); + assert_eq!( + second_counter_msg_recorded.change, + ConfigChange::UpdateMinHops(Hops::SixHops) + ); + let second_counter_msg_recorded = respondent_recording.get_record::(2); + assert_eq!( + second_counter_msg_recorded.change, + ConfigChange::UpdateWallets(WalletPair { + consuming_wallet: make_wallet("abc"), + earning_wallet: make_wallet("def") + }) + ); + assert_eq!(respondent_recording.len(), 3); + let subject_recording = subject_recording_arc.lock().unwrap(); + let first_subject_recorded_msg = subject_recording.get_record::(0); + assert_eq!(first_subject_recorded_msg, &msg_3); + let second_subject_recorded_msg = subject_recording.get_record::(1); + assert_eq!(second_subject_recorded_msg, &msg_2); + let third_subject_recording_msg = subject_recording.get_record::(2); + assert_eq!(third_subject_recording_msg, &msg_1); + assert_eq!(subject_recording.len(), 3) + } } diff --git a/node/src/test_utils/recorder_counter_msgs.rs b/node/src/test_utils/recorder_counter_msgs.rs new file mode 100644 index 000000000..11adeff67 --- /dev/null +++ b/node/src/test_utils/recorder_counter_msgs.rs @@ -0,0 +1,115 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +#![cfg(test)] + +use crate::test_utils::recorder_stop_conditions::{ForcedMatchable, MsgIdentification}; +use actix::dev::ToEnvelope; +use actix::{Addr, Handler, Message, Recipient}; +use std::any::{type_name, TypeId}; +use std::cell::RefCell; +use std::collections::hash_map::Entry; +use std::collections::HashMap; + +pub trait CounterMsgGear: Send { + fn try_send(&self); +} + +pub struct SendableCounterMsgWithRecipient +where + Msg: Message + Send, + Msg::Result: Send, +{ + msg_opt: RefCell>, + recipient: Recipient, +} + +impl CounterMsgGear for SendableCounterMsgWithRecipient +where + Msg: Message + Send, + Msg::Result: Send, +{ + fn try_send(&self) { + let msg = self.msg_opt.take().unwrap(); + self.recipient.try_send(msg).unwrap() + } +} + +impl SendableCounterMsgWithRecipient +where + Msg: Message + Send + 'static, + Msg::Result: Send, +{ + pub fn new(msg: Msg, recipient: Recipient) -> SendableCounterMsgWithRecipient { + Self { + msg_opt: RefCell::new(Some(msg)), + recipient, + } + } +} + +pub struct CounterMsgSetup { + // Leave them private + trigger_msg_type_id: TypeId, + condition: MsgIdentification, + msg_gear: Box, +} + +impl CounterMsgSetup { + pub fn new( + trigger_msg_type_id: TypeId, + trigger_msg_id_method: MsgIdentification, + counter_msg: Msg, + counter_msg_actor_addr: &Addr, + ) -> Self + where + Msg: Message + Send + 'static, + Msg::Result: Send, + Actor: actix::Actor + Handler, + Actor::Context: ToEnvelope, + { + let msg_gear = Box::new(SendableCounterMsgWithRecipient::new( + counter_msg, + counter_msg_actor_addr.clone().recipient(), + )); + Self { + trigger_msg_type_id, + condition: trigger_msg_id_method, + msg_gear, + } + } +} + +#[derive(Default)] +pub struct CounterMessages { + msgs: HashMap>, +} + +impl CounterMessages { + pub fn search_for_msg_setup(&mut self, msg: &Msg) -> Option> + where + Msg: ForcedMatchable + 'static, + { + let type_id = msg.correct_msg_type_id(); + if let Some(msgs_vec) = self.msgs.get_mut(&type_id) { + msgs_vec + .iter_mut() + .position(|cm_setup| cm_setup.condition.resolve_condition(msg)) + .map(|idx| { + let matching_counter_msg = msgs_vec.remove(idx); + matching_counter_msg.msg_gear + }) + } else { + None + } + } + + pub fn add_msg(&mut self, counter_msg_setup: CounterMsgSetup) { + let type_id = counter_msg_setup.trigger_msg_type_id; + match self.msgs.entry(type_id) { + Entry::Occupied(mut existing) => existing.get_mut().push(counter_msg_setup), + Entry::Vacant(vacant) => { + vacant.insert(vec![counter_msg_setup]); + } + } + } +} diff --git a/node/src/test_utils/recorder_stop_conditions.rs b/node/src/test_utils/recorder_stop_conditions.rs index 88c9b757a..1120b7e88 100644 --- a/node/src/test_utils/recorder_stop_conditions.rs +++ b/node/src/test_utils/recorder_stop_conditions.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +// Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. #![cfg(test)] @@ -6,16 +6,16 @@ use itertools::Itertools; use std::any::{Any, TypeId}; pub enum StopConditions { - Any(Vec), - All(Vec), + Any(Vec), + All(Vec), } -pub enum StopCondition { - StopOnType(TypeId), - StopOnMatch { +pub enum MsgIdentification { + ByType(TypeId), + ByMatch { exemplar: BoxedMsgExpected, }, - StopOnPredicate { + ByPredicate { predicate: Box bool + Send>, }, } @@ -35,7 +35,7 @@ impl StopConditions { } fn resolve_any + Send + 'static>( - conditions: &Vec, + conditions: &Vec, msg: &T, ) -> bool { conditions @@ -44,7 +44,7 @@ impl StopConditions { } fn resolve_all + Send + 'static>( - conditions: &mut Vec, + conditions: &mut Vec, msg: &T, ) -> bool { let indexes_to_remove = Self::indexes_of_matched_conditions(conditions, msg); @@ -53,7 +53,7 @@ impl StopConditions { } fn indexes_of_matched_conditions + Send + 'static>( - conditions: &[StopCondition], + conditions: &[MsgIdentification], msg: &T, ) -> Vec { conditions @@ -69,7 +69,7 @@ impl StopConditions { } fn remove_matched_conditions( - conditions: &mut Vec, + conditions: &mut Vec, indexes_to_remove: Vec, ) { if !indexes_to_remove.is_empty() { @@ -84,43 +84,43 @@ impl StopConditions { } } -impl StopCondition { - fn resolve_condition + Send + 'static>(&self, msg: &T) -> bool { +impl MsgIdentification { + pub fn resolve_condition + Send + 'static>(&self, msg: &Msg) -> bool { match self { - StopCondition::StopOnType(type_id) => Self::matches_stop_on_type::(msg, *type_id), - StopCondition::StopOnMatch { exemplar } => { - Self::matches_stop_on_match::(exemplar, msg) + MsgIdentification::ByType(type_id) => Self::matches_by_type::(msg, *type_id), + MsgIdentification::ByMatch { exemplar } => { + Self::matches_completely::(exemplar, msg) } - StopCondition::StopOnPredicate { predicate } => { - Self::matches_stop_on_predicate(predicate.as_ref(), msg) + MsgIdentification::ByPredicate { predicate } => { + Self::matches_by_predicate(predicate.as_ref(), msg) } } } - fn matches_stop_on_type>(msg: &T, expected_type_id: TypeId) -> bool { + fn matches_by_type>(msg: &Msg, expected_type_id: TypeId) -> bool { let correct_msg_type_id = msg.correct_msg_type_id(); correct_msg_type_id == expected_type_id } - fn matches_stop_on_match + 'static + Send>( + fn matches_completely + 'static + Send>( exemplar: &BoxedMsgExpected, - msg: &T, + msg: &Msg, ) -> bool { - if let Some(downcast_exemplar) = exemplar.downcast_ref::() { + if let Some(downcast_exemplar) = exemplar.downcast_ref::() { return downcast_exemplar == msg; } false } - fn matches_stop_on_predicate( + fn matches_by_predicate( predicate: &dyn Fn(RefMsgExpected) -> bool, - msg: &T, + msg: &Msg, ) -> bool { predicate(msg as RefMsgExpected) } } -pub trait ForcedMatchable: PartialEq + Send { +pub trait ForcedMatchable: PartialEq + Send { fn correct_msg_type_id(&self) -> TypeId; } @@ -139,7 +139,7 @@ where impl PartialEq for PretendedMatchableWrapper { fn eq(&self, _other: &Self) -> bool { panic!( - r#"You requested StopCondition::StopOnMatch for message + r#"You requested MsgIdentification::ByMatch for message that does not implement PartialEq. Consider two other options: matching the type simply by its TypeId or using a predicate."# @@ -150,7 +150,7 @@ impl PartialEq for PretendedMatchableWrapper { #[macro_export] macro_rules! match_every_type_id{ ($($single_message: ident),+) => { - StopConditions::All(vec![$(StopCondition::StopOnType(TypeId::of::<$single_message>())),+]) + StopConditions::All(vec![$(MsgIdentification::ByType(TypeId::of::<$single_message>())),+]) } } @@ -159,7 +159,7 @@ mod tests { use crate::accountant::{ResponseSkeleton, ScanError, ScanForNewPayables}; use crate::daemon::crash_notification::CrashNotification; use crate::sub_lib::peer_actors::{NewPublicIp, StartMessage}; - use crate::test_utils::recorder_stop_conditions::{StopCondition, StopConditions}; + use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; use std::any::TypeId; use std::net::{IpAddr, Ipv4Addr}; use std::vec; @@ -167,34 +167,34 @@ mod tests { #[test] fn remove_matched_conditions_works_with_unsorted_indexes() { let mut conditions = vec![ - StopCondition::StopOnType(TypeId::of::()), - StopCondition::StopOnType(TypeId::of::()), - StopCondition::StopOnType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), ]; let indexes = vec![2, 0]; StopConditions::remove_matched_conditions(&mut conditions, indexes); assert_eq!(conditions.len(), 1); - let type_id = if let StopCondition::StopOnType(type_id) = conditions[0] { + let type_id = if let MsgIdentification::ByType(type_id) = conditions[0] { type_id } else { - panic!("expected StopOnType but got a different variant") + panic!("expected ByType but got a different variant") }; assert_eq!(type_id, TypeId::of::()) } #[test] fn stop_on_match_works() { - let mut cond1 = StopConditions::All(vec![StopCondition::StopOnMatch { + let mut cond1 = StopConditions::All(vec![MsgIdentification::ByMatch { exemplar: Box::new(StartMessage {}), }]); - let mut cond2 = StopConditions::All(vec![StopCondition::StopOnMatch { + let mut cond2 = StopConditions::All(vec![MsgIdentification::ByMatch { exemplar: Box::new(NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(1, 8, 6, 4)), }), }]); - let mut cond3 = StopConditions::All(vec![StopCondition::StopOnMatch { + let mut cond3 = StopConditions::All(vec![MsgIdentification::ByMatch { exemplar: Box::new(NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(44, 2, 3, 1)), }), @@ -219,7 +219,7 @@ mod tests { #[test] fn stop_on_predicate_works() { - let mut cond_set = StopConditions::All(vec![StopCondition::StopOnPredicate { + let mut cond_set = StopConditions::All(vec![MsgIdentification::ByPredicate { predicate: Box::new(|msg| { let scan_err_msg: &ScanError = msg.downcast_ref().unwrap(); scan_err_msg.scan_type == ScanType::PendingPayables @@ -249,8 +249,8 @@ mod tests { #[test] fn match_any_works_with_every_matching_condition_and_no_need_to_take_elements_out() { let mut cond_set = StopConditions::Any(vec![ - StopCondition::StopOnType(TypeId::of::()), - StopCondition::StopOnMatch { + MsgIdentification::ByType(TypeId::of::()), + MsgIdentification::ByMatch { exemplar: Box::new(StartMessage {}), }, ]); @@ -291,7 +291,7 @@ mod tests { #[test] fn match_all_with_conditions_gradually_eliminated_until_vector_is_emptied_and_it_is_match() { let mut cond_set = StopConditions::All(vec![ - StopCondition::StopOnPredicate { + MsgIdentification::ByPredicate { predicate: Box::new(|msg| { if let Some(ip_msg) = msg.downcast_ref::() { ip_msg.new_ip.is_ipv4() @@ -300,7 +300,7 @@ mod tests { } }), }, - StopCondition::StopOnMatch { + MsgIdentification::ByMatch { exemplar: Box::new(ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -308,7 +308,7 @@ mod tests { }), }), }, - StopCondition::StopOnType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), ]); let tested_msg_1 = ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { @@ -323,8 +323,8 @@ mod tests { match &cond_set { StopConditions::All(conds) => { assert_eq!(conds.len(), 2); - assert!(matches!(conds[0], StopCondition::StopOnPredicate { .. })); - assert!(matches!(conds[1], StopCondition::StopOnType(_))); + assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); + assert!(matches!(conds[1], MsgIdentification::ByType(_))); } StopConditions::Any(_) => panic!("Stage 1: expected StopConditions::All, not ...Any"), } From b4b4cb1b277d300721cdbd592780078b776e3fb1 Mon Sep 17 00:00:00 2001 From: Bert Date: Sat, 26 Apr 2025 23:24:23 +0200 Subject: [PATCH 07/49] GH-602: removed obfuscated and made right the formerly developed StopConditions --- masq_lib/src/type_obfuscation.rs | 83 -------------------------------- 1 file changed, 83 deletions(-) delete mode 100644 masq_lib/src/type_obfuscation.rs diff --git a/masq_lib/src/type_obfuscation.rs b/masq_lib/src/type_obfuscation.rs deleted file mode 100644 index 1f3c79258..000000000 --- a/masq_lib/src/type_obfuscation.rs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -use std::any::TypeId; -use std::mem::transmute; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct Obfuscated { - type_id: TypeId, - bytes: Vec, -} - -impl Obfuscated { - // Although we're asking the compiler for a cast between two types - // where one is generic and both could possibly be of a different - // size, which almost applies to an unsupported kind of operation, - // the compiler stays calm here. The use of vectors at the input as - // well as output lets us avoid the above depicted situation. - // - // If you wish to write an implementation allowing more arbitrary - // types on your own, instead of helping yourself by a library like - // 'bytemuck', consider these functions from the std library, - // 'mem::transmute_copy' or 'mem::forget()', which will renew - // the compiler's trust for you. However, the true adventure will - // begin when you are supposed to write code to realign the plain - // bytes backwards to your desired type... - - pub fn obfuscate_vector(data: Vec) -> Obfuscated { - let bytes = unsafe { transmute::, Vec>(data) }; - - Obfuscated { - type_id: TypeId::of::(), - bytes, - } - } - - pub fn expose_vector(self) -> Vec { - if self.type_id != TypeId::of::() { - panic!("Forbidden! You're trying to interpret obfuscated data as the wrong type.") - } - - unsafe { transmute::, Vec>(self.bytes) } - } - - // Proper casting from a non vec structure into a vector of bytes - // is difficult and ideally requires an involvement of a library - // like bytemuck. - // If you think we do need such cast, place other methods in here - // and don't remove the ones above because: - // a) bytemuck will force you to implement its 'Pod' trait which - // might imply an (at minimum) ugly implementation for a std - // type like a Vec because both the trait and the type have - // their definitions situated externally to our project, - // therefore you might need to solve it by introducing - // a super-trait from our code - // b) using our simple 'obfuscate_vector' function will always - // be fairly more efficient than if done with help of - // the other library -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn obfuscation_works() { - let data = vec!["I'm fearing of losing my entire identity".to_string()]; - - let obfuscated_data = Obfuscated::obfuscate_vector(data.clone()); - let fenix_like_data: Vec = obfuscated_data.expose_vector(); - - assert_eq!(data, fenix_like_data) - } - - #[test] - #[should_panic( - expected = "Forbidden! You're trying to interpret obfuscated data as the wrong type." - )] - fn obfuscation_attempt_to_reinterpret_to_wrong_type() { - let data = vec![0_u64]; - let obfuscated_data = Obfuscated::obfuscate_vector(data.clone()); - let _: Vec = obfuscated_data.expose_vector(); - } -} From d5f03148ebe86da73710b496c2190f17e18c46fa Mon Sep 17 00:00:00 2001 From: Bert Date: Sat, 26 Apr 2025 23:24:23 +0200 Subject: [PATCH 08/49] GH-602: removed obfuscated and made right the formerly developed StopConditions --- masq_lib/src/lib.rs | 1 - masq_lib/src/type_obfuscation.rs | 83 --- node/src/accountant/mod.rs | 483 +++++++++--------- node/src/accountant/payment_adjuster.rs | 3 +- node/src/accountant/scanners/mod.rs | 153 +++--- .../scanners/payable_scanner/msgs.rs | 20 +- node/src/accountant/scanners/test_utils.rs | 5 - node/src/blockchain/blockchain_bridge.rs | 41 +- node/src/proxy_server/mod.rs | 10 +- node/src/test_utils/mod.rs | 6 +- node/src/test_utils/recorder.rs | 150 ++++-- node/src/test_utils/recorder_counter_msgs.rs | 77 ++- .../test_utils/recorder_stop_conditions.rs | 173 ++++++- 13 files changed, 663 insertions(+), 542 deletions(-) delete mode 100644 masq_lib/src/type_obfuscation.rs diff --git a/masq_lib/src/lib.rs b/masq_lib/src/lib.rs index e5232b221..db70ff4cf 100644 --- a/masq_lib/src/lib.rs +++ b/masq_lib/src/lib.rs @@ -22,6 +22,5 @@ pub mod crash_point; pub mod data_version; pub mod shared_schema; pub mod test_utils; -pub mod type_obfuscation; pub mod ui_gateway; pub mod ui_traffic_converter; diff --git a/masq_lib/src/type_obfuscation.rs b/masq_lib/src/type_obfuscation.rs deleted file mode 100644 index 1f3c79258..000000000 --- a/masq_lib/src/type_obfuscation.rs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -use std::any::TypeId; -use std::mem::transmute; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct Obfuscated { - type_id: TypeId, - bytes: Vec, -} - -impl Obfuscated { - // Although we're asking the compiler for a cast between two types - // where one is generic and both could possibly be of a different - // size, which almost applies to an unsupported kind of operation, - // the compiler stays calm here. The use of vectors at the input as - // well as output lets us avoid the above depicted situation. - // - // If you wish to write an implementation allowing more arbitrary - // types on your own, instead of helping yourself by a library like - // 'bytemuck', consider these functions from the std library, - // 'mem::transmute_copy' or 'mem::forget()', which will renew - // the compiler's trust for you. However, the true adventure will - // begin when you are supposed to write code to realign the plain - // bytes backwards to your desired type... - - pub fn obfuscate_vector(data: Vec) -> Obfuscated { - let bytes = unsafe { transmute::, Vec>(data) }; - - Obfuscated { - type_id: TypeId::of::(), - bytes, - } - } - - pub fn expose_vector(self) -> Vec { - if self.type_id != TypeId::of::() { - panic!("Forbidden! You're trying to interpret obfuscated data as the wrong type.") - } - - unsafe { transmute::, Vec>(self.bytes) } - } - - // Proper casting from a non vec structure into a vector of bytes - // is difficult and ideally requires an involvement of a library - // like bytemuck. - // If you think we do need such cast, place other methods in here - // and don't remove the ones above because: - // a) bytemuck will force you to implement its 'Pod' trait which - // might imply an (at minimum) ugly implementation for a std - // type like a Vec because both the trait and the type have - // their definitions situated externally to our project, - // therefore you might need to solve it by introducing - // a super-trait from our code - // b) using our simple 'obfuscate_vector' function will always - // be fairly more efficient than if done with help of - // the other library -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn obfuscation_works() { - let data = vec!["I'm fearing of losing my entire identity".to_string()]; - - let obfuscated_data = Obfuscated::obfuscate_vector(data.clone()); - let fenix_like_data: Vec = obfuscated_data.expose_vector(); - - assert_eq!(data, fenix_like_data) - } - - #[test] - #[should_panic( - expected = "Forbidden! You're trying to interpret obfuscated data as the wrong type." - )] - fn obfuscation_attempt_to_reinterpret_to_wrong_type() { - let data = vec![0_u64]; - let obfuscated_data = Obfuscated::obfuscate_vector(data.clone()); - let _: Vec = obfuscated_data.expose_vector(); - } -} diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 39c678862..e60fdf03d 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1144,8 +1144,8 @@ mod tests { use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::{protect_payables_in_test, NewPayableScanDynIntervalComputerMock}; - use crate::accountant::scanners::{BeginScanError}; + use crate::accountant::scanners::test_utils::{NewPayableScanDynIntervalComputerMock}; + use crate::accountant::scanners::{AccessibleScanner, BeginScanError, PayableScanner}; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; @@ -1160,7 +1160,7 @@ mod tests { use crate::database::test_utils::transaction_wrapper_mock::TransactionInnerWrapperMockBuilder; use crate::db_config::config_dao::ConfigDaoRecord; use crate::db_config::mocks::ConfigDaoMock; - use crate::match_every_type_id; + use crate::{match_lazily_every_type_id, setup_for_counter_msg_triggered_via_type_id, setup_for_counter_msg_triggered_via_specific_msg_id_method}; use crate::sub_lib::accountant::{ ExitServiceConsumed, PaymentThresholds, RoutingServiceConsumed, ScanIntervals, DEFAULT_EARNING_WALLET, DEFAULT_PAYMENT_THRESHOLDS, @@ -1168,7 +1168,7 @@ mod tests { use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::neighborhood::ConfigChange; use crate::sub_lib::neighborhood::{Hops, WalletPair}; - use crate::test_utils::recorder::make_recorder; + use crate::test_utils::recorder::{make_recorder, SetUpCounterMsgs}; use crate::test_utils::recorder::peer_actors_builder; use crate::test_utils::recorder::Recorder; use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; @@ -1207,6 +1207,7 @@ mod tests { use crate::accountant::scanners::local_test_utils::{NullScanner, ScannerMock}; use crate::accountant::scanners::scan_schedulers::{NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; + use crate::test_utils::recorder_counter_msgs::SingleCounterMsgSetup; impl Handler> for Accountant { type Result = (); @@ -1521,7 +1522,7 @@ mod tests { .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge = blockchain_bridge - .system_stop_conditions(match_every_type_id!(RequestTransactionReceipts)); + .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)); let blockchain_bridge_addr = blockchain_bridge.start(); subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); let subject_addr = subject.start(); @@ -1605,7 +1606,7 @@ mod tests { let is_adjustment_required_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let instructions_recipient = blockchain_bridge - .system_stop_conditions(match_every_type_id!(OutboundPaymentsInstructions)) + .system_stop_conditions(match_lazily_every_type_id!(OutboundPaymentsInstructions)) .start() .recipient(); let mut subject = AccountantBuilder::default().build(); @@ -1626,7 +1627,7 @@ mod tests { let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp); let accounts = vec![account_1, account_2]; let msg = BlockchainAgentWithContextMessage { - protected_qualified_payables: protect_payables_in_test(accounts.clone()), + qualified_payables: accounts.clone(), agent: Box::new(agent), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1641,8 +1642,8 @@ mod tests { let (blockchain_agent_with_context_msg_actual, logger_clone) = is_adjustment_required_params.remove(0); assert_eq!( - blockchain_agent_with_context_msg_actual.protected_qualified_payables, - protect_payables_in_test(accounts.clone()) + blockchain_agent_with_context_msg_actual.qualified_payables, + accounts.clone() ); assert_eq!( blockchain_agent_with_context_msg_actual.response_skeleton_opt, @@ -1698,7 +1699,7 @@ mod tests { let adjust_payments_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let report_recipient = blockchain_bridge - .system_stop_conditions(match_every_type_id!(OutboundPaymentsInstructions)) + .system_stop_conditions(match_lazily_every_type_id!(OutboundPaymentsInstructions)) .start() .recipient(); let mut subject = AccountantBuilder::default().build(); @@ -1719,12 +1720,10 @@ mod tests { let agent_id_stamp_first_phase = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp_first_phase); - let initial_unadjusted_accounts = protect_payables_in_test(vec![ - unadjusted_account_1.clone(), - unadjusted_account_2.clone(), - ]); + let initial_unadjusted_accounts = + vec![unadjusted_account_1.clone(), unadjusted_account_2.clone()]; let msg = BlockchainAgentWithContextMessage { - protected_qualified_payables: initial_unadjusted_accounts.clone(), + qualified_payables: initial_unadjusted_accounts.clone(), agent: Box::new(agent), response_skeleton_opt: Some(response_skeleton), }; @@ -1764,7 +1763,7 @@ mod tests { assert_eq!( actual_prepared_adjustment .original_setup_msg - .protected_qualified_payables, + .qualified_payables, initial_unadjusted_accounts ); assert_eq!( @@ -1948,7 +1947,7 @@ mod tests { assert_eq!( message, &QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(qualified_payables), + qualified_payables, consuming_wallet, response_skeleton_opt: None, } @@ -2215,17 +2214,34 @@ mod tests { let time_after = SystemTime::now(); let notify_later_receivable_params = notify_later_receivable_params_arc.lock().unwrap(); let tlh = TestLogHandler::new(); - tlh.exists_log_containing(&format!( - "DEBUG: {test_name}: There was nothing to process during Receivables scan." - )); - assert_start_scan_params_after_called_twice( - "receivable", - test_name, - &tlh, - &earning_wallet, - time_before, - time_after, - &start_scan_params_arc, + let mut start_scan_params = start_scan_params_arc.lock().unwrap(); + let ( + first_attempt_wallet, + first_attempt_timestamp, + first_attempt_response_skeleton_opt, + first_attempt_logger, + ) = start_scan_params.remove(0); + let ( + second_attempt_wallet, + second_attempt_timestamp, + second_attempt_response_skeleton_opt, + second_attempt_logger, + ) = start_scan_params.remove(0); + assert_eq!(first_attempt_wallet, earning_wallet); + assert_eq!(second_attempt_wallet, earning_wallet); + assert!(time_before <= first_attempt_timestamp); + assert!(first_attempt_timestamp <= second_attempt_timestamp); + assert!(second_attempt_timestamp <= time_after); + assert_eq!(first_attempt_response_skeleton_opt, None); + assert_eq!(second_attempt_response_skeleton_opt, None); + assert!(start_scan_params.is_empty()); + debug!( + first_attempt_logger, + "first attempt verifying receivable scanner" + ); + debug!( + second_attempt_logger, + "second attempt verifying receivable scanner" ); assert_eq!( *notify_later_receivable_params, @@ -2243,227 +2259,210 @@ mod tests { Duration::from_millis(99) ), ] - ) + ); + tlh.exists_log_containing(&format!( + "DEBUG: {test_name}: There was nothing to process during Receivables scan." + )); + tlh.exists_log_containing(&format!( + "DEBUG: {test_name}: first attempt verifying receivable scanner", + )); + tlh.exists_log_containing(&format!( + "DEBUG: {test_name}: second attempt verifying receivable scanner", + )); } // This test begins with the new payable scan, continues over the retry payable scan and ends // with another attempt for new payables which proves one complete cycle. #[test] fn periodical_scanning_for_payables_works() { - todo!( - "in order to write up this test, I'd have to make the recorder able to send messages \ - in a reaction to other message arrival" - ) - // init_test_logging(); - // let test_name = "periodical_scanning_for_payable_works"; - // let start_scan_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); - // let start_scan_payable_params_arc = Arc::new(Mutex::new(vec![])); - // let notify_later_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); - // let notify_payable_params_arc = Arc::new(Mutex::new(vec![])); - // let system = System::new(test_name); - // let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - // let blockchain_bridge = blockchain_bridge.system_stop_conditions(match_every_type_id!( - // RequestTransactionReceipts, - // RequestTransactionReceipts, - // QualifiedPayablesMessage - // )); - // let blockchain_bridge_addr = blockchain_bridge.start(); - // let consuming_wallet = make_paying_wallet(b"consuming"); - // let mut pending_payable_fingerprint = make_pending_payable_fingerprint(); - // let request_transaction_receipts_1 = RequestTransactionReceipts { - // pending_payable: vec![pending_payable_fingerprint], - // response_skeleton_opt: None, - // }; - // let pending_payable_scanner = ScannerMock::new() - // .started_at_result(None) - // .started_at_result(None) - // .started_at_result(None) - // .start_scan_params(&start_scan_pending_payable_params_arc) - // .start_scan_result(Ok(request_transaction_receipts_1.clone())); - // let qualified_payables_msg = QualifiedPayablesMessage { - // protected_qualified_payables: protect_payables_in_test(vec![make_payable_account(123)]), - // consuming_wallet: consuming_wallet.clone(), - // response_skeleton_opt: None, - // }; - // let payable_scanner = ScannerMock::new() - // .started_at_result(None) - // .started_at_result(None) - // .started_at_result(None) - // .start_scan_params(&start_scan_payable_params_arc) - // .start_scan_result(Ok(qualified_payables_msg.clone())) - // .start_scan_result(Err(BeginScanError::NothingToProcess)); - // let mut config = bc_from_earning_wallet(make_wallet("hi")); - // config.scan_intervals_opt = Some(ScanIntervals { - // payable_scan_interval: Duration::from_millis(97), - // pending_payable_scan_interval: Duration::from_secs(100), // We'll never run this scanner - // receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner - // }); - // let pending_payable = PendingPayable::new(make_wallet("abc"), make_tx_hash(123)); - // let mut subject = AccountantBuilder::default() - // .bootstrapper_config(config) - // .consuming_wallet(consuming_wallet.clone()) - // .logger(Logger::new(test_name)) - // .build(); - // subject.scanners.pending_payable = Box::new(pending_payable_scanner); - // subject.scanners.payable = Box::new(payable_scanner); - // subject.scanners.receivable = Box::new(NullScanner::new()); //skipping - // subject.scan_schedulers.update_scheduler( - // ScanType::PendingPayables, - // Some(Box::new( - // NotifyLaterHandleMock::::default() - // .notify_later_params(¬ify_later_pending_payables_params_arc) - // .capture_msg_and_let_it_fly_on(), - // )), - // None, - // ); - // subject.scan_schedulers.update_imminent_scheduler( - // ScanType::Payables, - // Some(Box::new( - // NotifyHandleMock::::default() - // .notify_params(¬ify_payable_params_arc) - // .capture_msg_and_let_it_fly_on(), - // )), - // ); - // subject.request_transaction_receipts_sub_opt = - // Some(blockchain_bridge_addr.clone().recipient()); - // subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); - // let subject_addr = subject.start(); - // let subject_subs = Accountant::make_subs_from(&subject_addr); - // - // subject_addr.try_send(ScanForNewPayables{response_skeleton_opt: None}).unwrap(); - // - // subject_addr.try_send(SentPayables{ payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct(pending_payable)]), response_skeleton_opt: None }).unwrap(); - // - // - // send_start_message!(subject_subs); - // - // let time_before = SystemTime::now(); - // system.run(); - // let time_after = SystemTime::now(); - // let tlh = TestLogHandler::new(); - // tlh.assert_logs_contain_in_order(vec![ - // &format!( - // "DEBUG: {test_name}: There was nothing to process during PendingPayables scan." - // ), - // &format!("DEBUG: {test_name}: There was nothing to process during Payables scan."), - // ]); - // let (pending_payable_first_attempt_timestamp, pending_payable_second_attempt_timestamp) = - // assert_start_scan_params_after_called_twice( - // "pending payable", - // test_name, - // &tlh, - // &consuming_wallet, - // time_before, - // time_after, - // &start_scan_pending_payable_params_arc, - // ); - // let (payable_first_attempt_timestamp, payable_second_attempt_timestamp) = - // assert_start_scan_params_after_called_twice( - // "payable", - // test_name, - // &tlh, - // &consuming_wallet, - // time_before, - // time_after, - // &start_scan_payable_params_arc, - // ); - // assert!(pending_payable_first_attempt_timestamp < payable_first_attempt_timestamp); - // assert!(pending_payable_second_attempt_timestamp < payable_second_attempt_timestamp); - // let notify_later_pending_payables_params = - // notify_later_pending_payables_params_arc.lock().unwrap(); - // assert_eq!( - // *notify_later_pending_payables_params, - // vec![ - // ( - // ScanForPendingPayables { - // response_skeleton_opt: None - // }, - // Duration::from_millis(97) - // ), - // ( - // ScanForPendingPayables { - // response_skeleton_opt: None - // }, - // Duration::from_millis(97) - // ), - // ( - // ScanForPendingPayables { - // response_skeleton_opt: None - // }, - // Duration::from_millis(97) - // ), - // ] - // ); - // let notify_payables_params = notify_payable_params_arc.lock().unwrap(); - // assert_eq!( - // *notify_payables_params, - // vec![ - // ScanForNewPayables { - // response_skeleton_opt: None - // }, - // ScanForNewPayables { - // response_skeleton_opt: None - // }, - // ScanForNewPayables { - // response_skeleton_opt: None - // } - // ] - // ); - // let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); - // let actual_requested_receipts_1 = - // blockchain_bridge_recording.get_record::(0); - // assert_eq!(actual_requested_receipts_1, &request_transaction_receipts_1); - // let actual_requested_receipts_2 = - // blockchain_bridge_recording.get_record::(1); - // assert_eq!(actual_requested_receipts_2, &request_transaction_receipts_2); - // let actual_qualified_payables = - // blockchain_bridge_recording.get_record::(2); - // assert_eq!(actual_qualified_payables, &qualified_payables_msg) - } - - fn assert_start_scan_params_after_called_twice( - scanner_name: &str, - test_name: &str, - tlh: &TestLogHandler, - wallet: &Wallet, - time_before: SystemTime, - time_after: SystemTime, - start_scan_params_arc: &Arc< - Mutex, Logger)>>, - >, - ) -> (SystemTime, SystemTime) { - let mut start_scan_params = start_scan_params_arc.lock().unwrap(); - let ( - first_attempt_wallet, - first_attempt_timestamp, - first_attempt_response_skeleton_opt, - first_attempt_logger, - ) = start_scan_params.remove(0); - let ( - second_attempt_wallet, - second_attempt_timestamp, - second_attempt_response_skeleton_opt, - second_attempt_logger, - ) = start_scan_params.remove(0); - assert_eq!(first_attempt_wallet, second_attempt_wallet); - assert_eq!(&second_attempt_wallet, wallet); - assert!(time_before <= first_attempt_timestamp); - assert!(first_attempt_timestamp <= second_attempt_timestamp); - assert!(second_attempt_timestamp <= time_after); - assert_eq!(first_attempt_response_skeleton_opt, None); - assert_eq!(second_attempt_response_skeleton_opt, None); - assert!(start_scan_params.is_empty()); - debug!(first_attempt_logger, "first attempt {}", scanner_name); - debug!(second_attempt_logger, "second attempt {}", scanner_name); + init_test_logging(); + let test_name = "periodical_scanning_for_payable_works"; + let start_scan_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); + let start_scan_payable_params_arc = Arc::new(Mutex::new(vec![])); + let notify_later_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); + let notify_payable_params_arc = Arc::new(Mutex::new(vec![])); + let system = System::new(test_name); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let payable_account = make_payable_account(123); + let qualified_payable = vec![payable_account.clone()]; + let consuming_wallet = make_paying_wallet(b"consuming"); + let agent = BlockchainAgentMock::default(); + let counter_msg_1 = BlockchainAgentWithContextMessage { + qualified_payables: qualified_payable.clone(), + agent: Box::new(agent), + response_skeleton_opt: None, + }; + let transaction_hash = make_tx_hash(789); + let creditor_wallet = make_wallet("blah"); + let counter_msg_2 = SentPayables { + payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( + PendingPayable::new(creditor_wallet, transaction_hash), + )]), + response_skeleton_opt: None, + }; + let tx_receipt = TxReceipt { + transaction_hash, + status: TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(369369), + block_number: 4444444444u64.into(), + }), + }; + let pending_payable_fingerprint = make_pending_payable_fingerprint(); + let counter_msg_4 = ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(tx_receipt), + pending_payable_fingerprint.clone(), + )], + response_skeleton_opt: None, + }; + let blockchain_bridge = + blockchain_bridge.system_stop_conditions(match_lazily_every_type_id!( + QualifiedPayablesMessage, + RequestTransactionReceipts //RequestTransactionReceipts + )); + let blockchain_bridge_addr = blockchain_bridge.start(); + let request_transaction_receipts = RequestTransactionReceipts { + pending_payable: vec![pending_payable_fingerprint], + response_skeleton_opt: None, + }; + let pending_payable_scanner = ScannerMock::new() + .started_at_result(None) + .start_scan_params(&start_scan_pending_payable_params_arc) + .start_scan_result(Ok(request_transaction_receipts.clone())) + .start_scan_result(Err(BeginScanError::NothingToProcess)); + let qualified_payables_msg = QualifiedPayablesMessage { + qualified_payables: qualified_payable.clone(), + consuming_wallet: consuming_wallet.clone(), + response_skeleton_opt: None, + }; + let payable_scanner = ScannerMock::new() + .started_at_result(None) + // Checking on both scanners within ScanForRetryPayable + .started_at_result(None) + .start_scan_params(&start_scan_payable_params_arc) + .start_scan_result(Ok(qualified_payables_msg.clone())) + .start_scan_result(Err(BeginScanError::NothingToProcess)) + .finish_scan_result(UiScanResult::Finished(None)); + let mut config = bc_from_earning_wallet(make_wallet("hi")); + config.scan_intervals_opt = Some(ScanIntervals { + // This simply means that we're gonna surplus this value (it abides by how many pending + // payable cycles have to go in between before the lastly submitted txs are confirmed), + payable_scan_interval: Duration::from_millis(10), + pending_payable_scan_interval: Duration::from_millis(50), + receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner + }); + let pending_payable = PendingPayable::new(make_wallet("abc"), make_tx_hash(123)); + let mut subject = AccountantBuilder::default() + .bootstrapper_config(config) + .consuming_wallet(consuming_wallet.clone()) + .logger(Logger::new(test_name)) + .build(); + subject.scanners.pending_payable = Box::new(pending_payable_scanner); + subject.scanners.payable = Box::new(payable_scanner); + subject.scanners.receivable = Box::new(NullScanner::new()); //skipping + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::::default() + .notify_later_params(¬ify_later_pending_payables_params_arc) + .capture_msg_and_let_it_fly_on(), + ); + subject.scan_schedulers.payable.new_payable_notify = Box::new( + NotifyHandleMock::::default() + .notify_params(¬ify_payable_params_arc) + .capture_msg_and_let_it_fly_on(), + ); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.clone().recipient()); + subject.outbound_payments_instructions_sub_opt = + Some(blockchain_bridge_addr.clone().recipient()); + subject.request_transaction_receipts_sub_opt = + Some(blockchain_bridge_addr.clone().recipient()); + let subject_addr = subject.start(); + let subject_subs = Accountant::make_subs_from(&subject_addr); + let set_up_counter_msgs = SetUpCounterMsgs::new(vec![ + setup_for_counter_msg_triggered_via_type_id!( + QualifiedPayablesMessage, + counter_msg_1, + &subject_addr + ), + setup_for_counter_msg_triggered_via_type_id!( + OutboundPaymentsInstructions, + counter_msg_2, + &subject_addr + ), + ]); + blockchain_bridge_addr + .try_send(set_up_counter_msgs) + .unwrap(); + + subject_addr + .try_send(ScanForNewPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + let time_before = SystemTime::now(); + system.run(); + let time_after = SystemTime::now(); + let tlh = TestLogHandler::new(); + let mut start_scan_payable_params = start_scan_payable_params_arc.lock().unwrap(); + let (wallet, timestamp, response_skeleton_opt, logger) = + start_scan_payable_params.remove(0); + assert_eq!(wallet, consuming_wallet); + assert!(time_before <= timestamp && timestamp <= time_after); + assert_eq!(response_skeleton_opt, None); + assert!(start_scan_payable_params.is_empty()); + debug!(logger, "verifying payable scanner logger"); + let mut start_scan_pending_payable_params = + start_scan_pending_payable_params_arc.lock().unwrap(); + let (wallet, timestamp, response_skeleton_opt, logger) = + start_scan_pending_payable_params.remove(0); + assert_eq!(wallet, consuming_wallet); + assert!(time_before <= timestamp && timestamp <= time_after); + assert_eq!(response_skeleton_opt, None); + assert!(start_scan_pending_payable_params.is_empty()); + debug!(logger, "verifying pending payable scanner logger"); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + let actual_qualified_payables_msg = + blockchain_bridge_recording.get_record::(0); + assert_eq!(actual_qualified_payables_msg, &qualified_payables_msg); + let actual_outbound_payment_instructions_msg = + blockchain_bridge_recording.get_record::(1); + assert_eq!( + actual_outbound_payment_instructions_msg.affordable_accounts, + vec![payable_account] + ); + let actual_requested_receipts_1 = + blockchain_bridge_recording.get_record::(2); + assert_eq!(actual_requested_receipts_1, &request_transaction_receipts); + let notify_later_pending_payables_params = + notify_later_pending_payables_params_arc.lock().unwrap(); + assert_eq!( + *notify_later_pending_payables_params, + vec![( + ScanForPendingPayables { + response_skeleton_opt: None + }, + Duration::from_millis(50) + ),] + ); + let notify_payables_params = notify_payable_params_arc.lock().unwrap(); + assert_eq!( + *notify_payables_params, + vec![ScanForNewPayables { + response_skeleton_opt: None + },] + ); tlh.exists_log_containing(&format!( - "DEBUG: {test_name}: first attempt {}", - scanner_name + "DEBUG: {test_name}: verifying payable scanner logger" )); tlh.exists_log_containing(&format!( - "DEBUG: {test_name}: second attempt {}", - scanner_name + "DEBUG: {test_name}: verifying pending payable scanner logger" )); - (first_attempt_timestamp, second_attempt_timestamp) + tlh.assert_logs_contain_in_order(vec![ + &format!( + "DEBUG: {test_name}: There was nothing to process during PendingPayables scan." + ), + &format!("DEBUG: {test_name}: There was nothing to process during Payables scan."), + ]); } #[test] @@ -2676,7 +2675,7 @@ mod tests { assert_eq!( message, &QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(qualified_payables), + qualified_payables, consuming_wallet, response_skeleton_opt, } @@ -2691,7 +2690,7 @@ mod tests { let payment_thresholds = PaymentThresholds::default(); let (blockchain_bridge, _, blockchain_bridge_recording) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge - .system_stop_conditions(match_every_type_id!( + .system_stop_conditions(match_lazily_every_type_id!( QualifiedPayablesMessage, QualifiedPayablesMessage )) @@ -2770,7 +2769,7 @@ mod tests { init_test_logging(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge - .system_stop_conditions(match_every_type_id!(RequestTransactionReceipts)) + .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)) .start(); let payable_fingerprint_1 = PendingPayableFingerprint { rowid: 555, diff --git a/node/src/accountant/payment_adjuster.rs b/node/src/accountant/payment_adjuster.rs index 519019e18..ba4cd40d1 100644 --- a/node/src/accountant/payment_adjuster.rs +++ b/node/src/accountant/payment_adjuster.rs @@ -73,7 +73,6 @@ mod tests { use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; use crate::accountant::scanners::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::protect_payables_in_test; use crate::accountant::test_utils::make_payable_account; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; @@ -86,7 +85,7 @@ mod tests { payable.balance_wei = 100_000_000; let agent = BlockchainAgentMock::default(); let setup_msg = BlockchainAgentWithContextMessage { - protected_qualified_payables: protect_payables_in_test(vec![payable]), + qualified_payables: vec![payable], agent: Box::new(agent), response_skeleton_opt: None, }; diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 756a2f5c6..6ef74c399 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -48,7 +48,7 @@ use std::time::{SystemTime}; use time::format_description::parse; use time::OffsetDateTime; use web3::types::H256; -use masq_lib::type_obfuscation::Obfuscated; +use crate::accountant::scanners::local_test_utils::NullScanner; use crate::accountant::scanners::payable_scanner::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; use crate::accountant::scanners::payable_scanner::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; @@ -400,9 +400,9 @@ impl InaccessibleScanner for Payab "Chose {} qualified debts to pay", qualified_payables.len() ); - let protected_payables = self.protect_payables(qualified_payables); + let outgoing_msg = QualifiedPayablesMessage::new( - protected_payables, + qualified_payables, consuming_wallet.clone(), response_skeleton_opt, ); @@ -477,15 +477,11 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { .payment_adjuster .search_for_indispensable_adjustment(&msg, logger) { - Ok(None) => { - let protected = msg.protected_qualified_payables; - let unprotected = self.expose_payables(protected); - Ok(Either::Left(OutboundPaymentsInstructions::new( - unprotected, - msg.agent, - msg.response_skeleton_opt, - ))) - } + Ok(None) => Ok(Either::Left(OutboundPaymentsInstructions::new( + msg.qualified_payables, + msg.agent, + msg.response_skeleton_opt, + ))), Ok(Some(adjustment)) => Ok(Either::Right(PreparedAdjustment::new(msg, adjustment))), Err(_e) => todo!("be implemented with GH-711"), } @@ -751,14 +747,6 @@ impl PayableScanner { panic!("{}", msg) }; } - - pub(super) fn protect_payables(&self, payables: Vec) -> Obfuscated { - Obfuscated::obfuscate_vector(payables) - } - - pub(super) fn expose_payables(&self, obfuscated: Obfuscated) -> Vec { - obfuscated.expose_vector() - } } pub struct PendingPayableScanner { @@ -1288,14 +1276,14 @@ impl BeginScanError { } } -// Note that this location was chosen because these mocks below need to implement a private trait +// Note that this location was chosen because the following mocks need to implement a private trait // from this file #[cfg(test)] pub mod local_test_utils { use crate::accountant::scanners::payable_scanner::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::{ AccessibleScanner, BeginScanError, InaccessibleScanner, MultistageDualPayableScanner, - PreparedAdjustment, PrivateScanStarter, ScanWithStarter, + PreparedAdjustment, PrivateScanStarter, ScanWithStarter, Scanners, SolvencySensitivePaymentInstructor, StartableAccessibleScanner, UiScanResult, }; use crate::accountant::BlockchainAgentWithContextMessage; @@ -1309,30 +1297,6 @@ pub mod local_test_utils { use std::sync::{Arc, Mutex}; use std::time::SystemTime; - macro_rules! formal_traits_for_payable_mid_scan_msg_handling { - ($scanner:ty) => { - impl MultistageDualPayableScanner for $scanner {} - - impl SolvencySensitivePaymentInstructor for $scanner { - fn try_skipping_payment_adjustment( - &self, - _msg: BlockchainAgentWithContextMessage, - _logger: &Logger, - ) -> Result, String> { - intentionally_blank!() - } - - fn perform_payment_adjustment( - &self, - _setup: PreparedAdjustment, - _logger: &Logger, - ) -> OutboundPaymentsInstructions { - intentionally_blank!() - } - } - }; - } - pub struct NullScanner {} impl @@ -1350,7 +1314,7 @@ pub mod local_test_utils { StartMessage: Message, { fn scan_starter(&mut self) -> PrivateScanStarter { - todo!() + unimplemented!("Not needed yet") } } @@ -1378,7 +1342,25 @@ pub mod local_test_utils { as_any_ref_in_trait_impl!(); } - formal_traits_for_payable_mid_scan_msg_handling!(NullScanner); + impl MultistageDualPayableScanner for NullScanner {} + + impl SolvencySensitivePaymentInstructor for NullScanner { + fn try_skipping_payment_adjustment( + &self, + _msg: BlockchainAgentWithContextMessage, + _logger: &Logger, + ) -> Result, String> { + intentionally_blank!() + } + + fn perform_payment_adjustment( + &self, + _setup: PreparedAdjustment, + _logger: &Logger, + ) -> OutboundPaymentsInstructions { + intentionally_blank!() + } + } impl InaccessibleScanner for NullScanner where @@ -1411,8 +1393,8 @@ pub mod local_test_utils { pub struct ScannerMock { start_scan_params: Arc, Logger)>>>, start_scan_results: RefCell>>, - end_scan_params: Arc>>, - end_scan_results: RefCell>, + finish_scan_params: Arc>>, + finish_scan_results: RefCell>, started_at_results: RefCell>>, stop_system_after_last_message: RefCell, } @@ -1473,11 +1455,11 @@ pub mod local_test_utils { EndMessage: Message, { fn finish_scan(&mut self, message: EndMessage, _logger: &Logger) -> UiScanResult { - self.end_scan_params.lock().unwrap().push(message); + self.finish_scan_params.lock().unwrap().push(message); if self.is_allowed_to_stop_the_system() && self.is_last_message() { System::current().stop(); } - self.end_scan_results.borrow_mut().remove(0) + self.finish_scan_results.borrow_mut().remove(0) } fn scan_started_at(&self) -> Option { @@ -1504,8 +1486,8 @@ pub mod local_test_utils { Self { start_scan_params: Arc::new(Mutex::new(vec![])), start_scan_results: RefCell::new(vec![]), - end_scan_params: Arc::new(Mutex::new(vec![])), - end_scan_results: RefCell::new(vec![]), + finish_scan_params: Arc::new(Mutex::new(vec![])), + finish_scan_results: RefCell::new(vec![]), started_at_results: RefCell::new(vec![]), stop_system_after_last_message: RefCell::new(false), } @@ -1529,6 +1511,11 @@ pub mod local_test_utils { self } + pub fn finish_scan_result(self, result: UiScanResult) -> Self { + self.finish_scan_results.borrow_mut().push(result); + self + } + pub fn stop_the_system_after_last_msg(self) -> Self { self.stop_system_after_last_message.replace(true); self @@ -1543,15 +1530,46 @@ pub mod local_test_utils { } pub fn is_last_message_from_start_scan(&self) -> bool { - self.start_scan_results.borrow().len() == 1 && self.end_scan_results.borrow().is_empty() + self.start_scan_results.borrow().len() == 1 + && self.finish_scan_results.borrow().is_empty() } pub fn is_last_message_from_end_scan(&self) -> bool { - self.end_scan_results.borrow().len() == 1 && self.start_scan_results.borrow().is_empty() + self.finish_scan_results.borrow().len() == 1 + && self.start_scan_results.borrow().is_empty() } } - formal_traits_for_payable_mid_scan_msg_handling!(ScannerMock); + impl MultistageDualPayableScanner + for ScannerMock + { + } + + impl SolvencySensitivePaymentInstructor for ScannerMock { + fn try_skipping_payment_adjustment( + &self, + msg: BlockchainAgentWithContextMessage, + _logger: &Logger, + ) -> Result, String> { + // Always passes... + // It would be quite inconvenient if we had to add specialized features to the generic + // mock, plus this functionality can be tested better with the other components mocked, + // not the scanner itself. + Ok(Either::Left(OutboundPaymentsInstructions { + affordable_accounts: msg.qualified_payables, + agent: msg.agent, + response_skeleton_opt: msg.response_skeleton_opt, + })) + } + + fn perform_payment_adjustment( + &self, + _setup: PreparedAdjustment, + _logger: &Logger, + ) -> OutboundPaymentsInstructions { + intentionally_blank!() + } + } } #[cfg(test)] @@ -1564,7 +1582,6 @@ mod tests { use crate::accountant::scanners::payable_scanner::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PendingPayableMetadata; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; - use crate::accountant::scanners::test_utils::protect_payables_in_test; use crate::accountant::scanners::{AccessibleScanner, BeginScanError, InaccessibleScanner, PayableScanner, PendingPayableScanner, PrivateScanStarter, ReceivableScanner, ScanType, ScanWithStarter, ScannerCommon, Scanners}; use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanForNewPayables, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; @@ -1702,18 +1719,6 @@ mod tests { ); } - #[test] - fn protected_payables_can_be_cast_from_and_back_to_vec_of_payable_accounts_by_payable_scanner() - { - let initial_unprotected = vec![make_payable_account(123), make_payable_account(456)]; - let subject = PayableScannerBuilder::new().build(); - - let protected = subject.protect_payables(initial_unprotected.clone()); - let again_unprotected: Vec = subject.expose_payables(protected); - - assert_eq!(initial_unprotected, again_unprotected) - } - #[test] fn new_payable_scanner_can_initiate_a_scan() { init_test_logging(); @@ -1742,9 +1747,7 @@ mod tests { assert_eq!( result, Ok(QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test( - qualified_payable_accounts.clone() - ), + qualified_payables: qualified_payable_accounts.clone(), consuming_wallet, response_skeleton_opt: None, }) @@ -1848,9 +1851,7 @@ mod tests { assert_eq!( result, Ok(QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test( - qualified_payable_accounts.clone() - ), + qualified_payables: qualified_payable_accounts.clone(), consuming_wallet, response_skeleton_opt: None, }) diff --git a/node/src/accountant/scanners/payable_scanner/msgs.rs b/node/src/accountant/scanners/payable_scanner/msgs.rs index fcdfc4430..49bb93909 100644 --- a/node/src/accountant/scanners/payable_scanner/msgs.rs +++ b/node/src/accountant/scanners/payable_scanner/msgs.rs @@ -1,27 +1,27 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; use crate::accountant::{ResponseSkeleton, SkeletonOptHolder}; use crate::sub_lib::wallet::Wallet; use actix::Message; -use masq_lib::type_obfuscation::Obfuscated; use std::fmt::Debug; #[derive(Debug, Message, PartialEq, Eq, Clone)] pub struct QualifiedPayablesMessage { - pub protected_qualified_payables: Obfuscated, + pub qualified_payables: Vec, pub consuming_wallet: Wallet, pub response_skeleton_opt: Option, } impl QualifiedPayablesMessage { pub(in crate::accountant) fn new( - protected_qualified_payables: Obfuscated, + qualified_payables: Vec, consuming_wallet: Wallet, response_skeleton_opt: Option, ) -> Self { Self { - protected_qualified_payables, + qualified_payables, consuming_wallet, response_skeleton_opt, } @@ -36,20 +36,20 @@ impl SkeletonOptHolder for QualifiedPayablesMessage { #[derive(Message)] pub struct BlockchainAgentWithContextMessage { - pub protected_qualified_payables: Obfuscated, + pub qualified_payables: Vec, pub agent: Box, pub response_skeleton_opt: Option, } impl BlockchainAgentWithContextMessage { pub fn new( - qualified_payables: Obfuscated, - blockchain_agent: Box, + qualified_payables: Vec, + agent: Box, response_skeleton_opt: Option, ) -> Self { Self { - protected_qualified_payables: qualified_payables, - agent: blockchain_agent, + qualified_payables, + agent, response_skeleton_opt, } } @@ -67,7 +67,7 @@ mod tests { let cloned_agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(original_agent_id); Self { - protected_qualified_payables: self.protected_qualified_payables.clone(), + qualified_payables: self.qualified_payables.clone(), agent: Box::new(cloned_agent), response_skeleton_opt: self.response_skeleton_opt, } diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 87f8c8636..90d16f491 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -4,15 +4,10 @@ use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::scanners::scan_schedulers::NewPayableScanDynIntervalComputer; -use masq_lib::type_obfuscation::Obfuscated; use std::cell::RefCell; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; -pub fn protect_payables_in_test(payables: Vec) -> Obfuscated { - Obfuscated::obfuscate_vector(payables) -} - #[derive(Default)] pub struct NewPayableScanDynIntervalComputerMock { compute_interval_params: Arc>>, diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index fdf636efd..1f82b759c 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -267,7 +267,7 @@ impl BlockchainBridge { .map_err(|e| format!("Blockchain agent build error: {:?}", e)) .and_then(move |agent| { let outgoing_message = BlockchainAgentWithContextMessage::new( - incoming_message.protected_qualified_payables, + incoming_message.qualified_payables, agent, incoming_message.response_skeleton_opt, ); @@ -551,7 +551,6 @@ mod tests { use crate::accountant::db_access_objects::utils::from_time_t; use crate::accountant::scanners::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::accountant::scanners::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::protect_payables_in_test; use crate::accountant::test_utils::{make_payable_account, make_pending_payable_fingerprint}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::BlockchainInterfaceWeb3; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError::TransactionID; @@ -566,7 +565,7 @@ mod tests { make_blockchain_interface_web3, make_tx_hash, ReceiptResponseBuilder, }; use crate::db_config::persistent_configuration::PersistentConfigError; - use crate::match_every_type_id; + use crate::match_lazily_every_type_id; use crate::node_test_utils::check_timestamp; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; @@ -723,9 +722,8 @@ mod tests { false, ); subject.payable_payments_setup_subs_opt = Some(accountant_recipient); - let qualified_payables = protect_payables_in_test(qualified_payables.clone()); let qualified_payables_msg = QualifiedPayablesMessage { - protected_qualified_payables: qualified_payables.clone(), + qualified_payables: qualified_payables.clone(), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11122, @@ -745,7 +743,7 @@ mod tests { let blockchain_agent_with_context_msg_actual: &BlockchainAgentWithContextMessage = accountant_received_payment.get_record(0); assert_eq!( - blockchain_agent_with_context_msg_actual.protected_qualified_payables, + blockchain_agent_with_context_msg_actual.qualified_payables, qualified_payables ); assert_eq!( @@ -807,9 +805,8 @@ mod tests { false, ); subject.payable_payments_setup_subs_opt = Some(accountant_recipient); - let qualified_payables = protect_payables_in_test(vec![]); let qualified_payables_msg = QualifiedPayablesMessage { - protected_qualified_payables: qualified_payables, + qualified_payables: vec![make_payable_account(123)], consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11122, @@ -857,7 +854,7 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(SentPayables)) + .system_stop_conditions(match_lazily_every_type_id!(SentPayables)) .start(); let wallet_account = make_wallet("blah"); let consuming_wallet = make_paying_wallet(b"consuming_wallet"); @@ -948,7 +945,7 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(SentPayables)) + .system_stop_conditions(match_lazily_every_type_id!(SentPayables)) .start(); let wallet_account = make_wallet("blah"); let blockchain_interface = make_blockchain_interface_web3(port); @@ -1148,7 +1145,7 @@ mod tests { #[test] fn blockchain_bridge_processes_requests_for_a_complete_and_null_transaction_receipt() { let (accountant, _, accountant_recording_arc) = make_recorder(); - let accountant = accountant.system_stop_conditions(match_every_type_id!(ScanError)); + let accountant = accountant.system_stop_conditions(match_lazily_every_type_id!(ScanError)); let pending_payable_fingerprint_1 = make_pending_payable_fingerprint(); let hash_1 = pending_payable_fingerprint_1.hash; let hash_2 = make_tx_hash(78989); @@ -1238,7 +1235,7 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); let scan_error_recipient: Recipient = accountant_addr.clone().recipient(); let received_payments_subs: Recipient = accountant_addr.recipient(); @@ -1307,7 +1304,10 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ReportTransactionReceipts, ScanError)) + .system_stop_conditions(match_lazily_every_type_id!( + ReportTransactionReceipts, + ScanError + )) .start(); let report_transaction_receipt_recipient: Recipient = accountant_addr.clone().recipient(); @@ -1405,7 +1405,7 @@ mod tests { init_test_logging(); let (accountant, _, accountant_recording) = make_recorder(); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); let scan_error_recipient: Recipient = accountant_addr.clone().recipient(); let report_transaction_recipient: Recipient = @@ -1614,7 +1614,7 @@ mod tests { .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = - accountant.system_stop_conditions(match_every_type_id!(ReceivedPayments)); + accountant.system_stop_conditions(match_lazily_every_type_id!(ReceivedPayments)); let some_wallet = make_wallet("somewallet"); let recipient_wallet = make_wallet("recipient_wallet"); let amount = 996000000; @@ -1707,7 +1707,7 @@ mod tests { let (accountant, _, accountant_recording_arc) = make_recorder(); let accountant_addr = - accountant.system_stop_conditions(match_every_type_id!(ReceivedPayments)); + accountant.system_stop_conditions(match_lazily_every_type_id!(ReceivedPayments)); let earning_wallet = make_wallet("earning_wallet"); let amount = 996000000; let blockchain_interface = make_blockchain_interface_web3(port); @@ -1791,7 +1791,8 @@ mod tests { .ok_response(expected_response_logs, 1) .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); - let accountant_addr = accountant.system_stop_conditions(match_every_type_id!(ScanError)); + let accountant_addr = + accountant.system_stop_conditions(match_lazily_every_type_id!(ScanError)); let earning_wallet = make_wallet("earning_wallet"); let mut blockchain_interface = make_blockchain_interface_web3(port); blockchain_interface.logger = logger; @@ -1848,7 +1849,7 @@ mod tests { .err_response(-32005, "Blockheight too far in the past. Check params passed to eth_getLogs or eth_call requests.Range of blocks allowed for your plan: 1000", 0) .start(); let (accountant, _, accountant_recording_arc) = make_recorder(); - let accountant = accountant.system_stop_conditions(match_every_type_id!(ScanError)); + let accountant = accountant.system_stop_conditions(match_lazily_every_type_id!(ScanError)); let earning_wallet = make_wallet("earning_wallet"); let blockchain_interface = make_blockchain_interface_web3(port); let set_max_block_count_params_arc = Arc::new(Mutex::new(vec![])); @@ -2023,7 +2024,7 @@ mod tests { ); let system = System::new("test"); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); subject.received_payments_subs_opt = Some(accountant_addr.clone().recipient()); subject.scan_error_subs_opt = Some(accountant_addr.recipient()); @@ -2074,7 +2075,7 @@ mod tests { ); let system = System::new("test"); let accountant_addr = accountant - .system_stop_conditions(match_every_type_id!(ScanError)) + .system_stop_conditions(match_lazily_every_type_id!(ScanError)) .start(); subject.received_payments_subs_opt = Some(accountant_addr.clone().recipient()); subject.scan_error_subs_opt = Some(accountant_addr.recipient()); diff --git a/node/src/proxy_server/mod.rs b/node/src/proxy_server/mod.rs index 3cf09abed..6e772b31f 100644 --- a/node/src/proxy_server/mod.rs +++ b/node/src/proxy_server/mod.rs @@ -1348,7 +1348,7 @@ impl Hostname { #[cfg(test)] mod tests { use super::*; - use crate::match_every_type_id; + use crate::match_lazily_every_type_id; use crate::proxy_server::protocol_pack::ServerImpersonator; use crate::proxy_server::server_impersonator_http::ServerImpersonatorHttp; use crate::proxy_server::server_impersonator_tls::ServerImpersonatorTls; @@ -2610,8 +2610,8 @@ mod tests { let cryptde = main_cryptde(); let http_request = b"GET /index.html HTTP/1.1\r\nHost: nowhere.com\r\n\r\n"; let (proxy_server_mock, _, proxy_server_recording_arc) = make_recorder(); - let proxy_server_mock = - proxy_server_mock.system_stop_conditions(match_every_type_id!(AddRouteResultMessage)); + let proxy_server_mock = proxy_server_mock + .system_stop_conditions(match_lazily_every_type_id!(AddRouteResultMessage)); let route_query_response = None; let (neighborhood_mock, _, _) = make_recorder(); let neighborhood_mock = @@ -5213,7 +5213,7 @@ mod tests { ), }; let neighborhood_mock = neighborhood_mock - .system_stop_conditions(match_every_type_id!(RouteQueryMessage)) + .system_stop_conditions(match_lazily_every_type_id!(RouteQueryMessage)) .route_query_response(Some(route_query_response_expected.clone())); let cryptde = main_cryptde(); let mut subject = ProxyServer::new( @@ -5383,7 +5383,7 @@ mod tests { ), }; let neighborhood_mock = neighborhood_mock - .system_stop_conditions(match_every_type_id!( + .system_stop_conditions(match_lazily_every_type_id!( RouteQueryMessage, RouteQueryMessage, RouteQueryMessage diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index e2fd38419..d04546aa4 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -12,7 +12,7 @@ pub mod logfile_name_guard; pub mod neighborhood_test_utils; pub mod persistent_configuration_mock; pub mod recorder; -mod recorder_counter_msgs; +pub mod recorder_counter_msgs; pub mod recorder_stop_conditions; pub mod stream_connector_mock; pub mod tcp_wrapper_mocks; @@ -699,7 +699,7 @@ pub mod unshared_test_utils { { let (recorder, _, recording_arc) = make_recorder(); let recorder = match stopping_message { - Some(type_id) => recorder.system_stop_conditions(StopConditions::All(vec![ + Some(type_id) => recorder.system_stop_conditions(StopConditions::AllLazily(vec![ MsgIdentification::ByType(type_id), ])), // This will take care of stopping the system None => recorder, @@ -1018,7 +1018,7 @@ pub mod unshared_test_utils { // you've pasted in before at the other end. // 3) Using raw pointers to link the real memory address to your objects does not lead to good // results in all cases (It was found confusing and hard to be done correctly or even impossible - // to implement especially for references pointing to a dereferenced Box that was originally + // to implement, especially for references pointing to a dereferenced Box that was originally // supplied as an owned argument into the testing environment at the beginning, or we can // suspect the memory link already broken because of moves of the owned boxed instance // around the subjected code) diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index 64edc59ba..a559b6546 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -47,7 +47,9 @@ use crate::sub_lib::stream_handler_pool::DispatcherNodeQueryResponse; use crate::sub_lib::stream_handler_pool::TransmitDataMsg; use crate::sub_lib::ui_gateway::UiGatewaySubs; use crate::sub_lib::utils::MessageScheduler; -use crate::test_utils::recorder_counter_msgs::{CounterMessages, CounterMsgGear, CounterMsgSetup}; +use crate::test_utils::recorder_counter_msgs::{ + CounterMessages, CounterMsgGear, SingleCounterMsgSetup, +}; use crate::test_utils::recorder_stop_conditions::{ ForcedMatchable, MsgIdentification, PretendedMatchableWrapper, StopConditions, }; @@ -60,7 +62,7 @@ use actix::MessageResult; use actix::System; use actix::{Actor, Message}; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; -use std::any::{Any, TypeId}; +use std::any::{type_name, Any, TypeId}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; @@ -211,6 +213,16 @@ impl Handler for Recorder { matchable!(RouteQueryMessage); +impl Handler for Recorder { + type Result = (); + + fn handle(&mut self, msg: SetUpCounterMsgs, _ctx: &mut Self::Context) -> Self::Result { + msg.setups + .into_iter() + .for_each(|msg_setup| self.add_counter_msg(msg_setup)) + } +} + fn extract_response(responses: &mut Vec, err_msg: &str) -> T where T: Clone, @@ -267,7 +279,7 @@ impl Recorder { self } - pub fn add_counter_msg(mut self, counter_msg_setup: CounterMsgSetup) -> Self { + fn add_counter_msg(&mut self, counter_msg_setup: SingleCounterMsgSetup) { if let Some(counter_msgs) = self.expected_future_counter_msgs_opt.as_mut() { counter_msgs.add_msg(counter_msg_setup) } else { @@ -275,7 +287,6 @@ impl Recorder { counter_msgs.add_msg(counter_msg_setup); self.expected_future_counter_msgs_opt = Some(counter_msgs) } - self } fn start_system_killer(&mut self) { @@ -297,8 +308,8 @@ impl Recorder { self.record(msg); - if let Some(sendable_msg) = counter_msg_opt { - sendable_msg.try_send() + if let Some(sendable_msgs) = counter_msg_opt { + sendable_msgs.into_iter().for_each(|msg| msg.try_send()) } if kill_system { @@ -314,7 +325,7 @@ impl Recorder { self.handle_msg_t_m_p(PretendedMatchableWrapper(msg)) } - fn check_on_counter_msg(&mut self, msg: &M) -> Option> + fn check_on_counter_msg(&mut self, msg: &M) -> Option>> where M: ForcedMatchable + 'static, { @@ -379,8 +390,9 @@ impl Recording { match item_opt { Some(item) => Ok(&item.0), None => Err(format!( - "Message {:?} could not be downcast to the expected type", - item_box + "Message {:?} could not be downcast to the expected type {}.", + item_box, + type_name::() )), } } @@ -414,6 +426,27 @@ impl RecordAwaiter { } } +#[derive(Message)] +pub struct SetUpCounterMsgs { + // Trigger msg - it arrives at the Recorder from the Actor being tested and matches one of the + // msg ID methods. + // Counter msg - it is sent back from the Recorder when a trigger msg is recognized + // + // In general, the triggering is data driven. Shuffling with the setups of differently typed + // trigger messages can't have any adverse effect. + // + // However, setups of the same trigger message types compose clusters. + // Keep in mind these are tested over their ID method sequentially, according to the order + // in which they are fed into this vector, with the other messages ignored. + setups: Vec, +} + +impl SetUpCounterMsgs { + pub fn new(setups: Vec) -> Self { + Self { setups } + } +} + pub fn make_recorder() -> (Recorder, RecordAwaiter, Arc>) { let recorder = Recorder::new(); let awaiter = recorder.get_awaiter(); @@ -635,10 +668,14 @@ impl PeerActorsBuilder { mod tests { use super::*; use crate::blockchain::blockchain_bridge::BlockchainBridge; - use crate::match_every_type_id; use crate::sub_lib::neighborhood::{ConfigChange, Hops, WalletPair}; use crate::test_utils::make_wallet; use crate::test_utils::neighborhood_test_utils::make_ip; + use crate::test_utils::recorder_counter_msgs::SendableCounterMsgWithRecipient; + use crate::{ + match_lazily_every_type_id, setup_for_counter_msg_triggered_via_specific_msg_id_method, + setup_for_counter_msg_triggered_via_type_id, + }; use actix::Message; use actix::System; use masq_lib::messages::{ @@ -647,6 +684,7 @@ mod tests { use masq_lib::ui_gateway::MessageTarget; use std::any::TypeId; use std::net::{IpAddr, Ipv4Addr}; + use std::vec; #[derive(Debug, PartialEq, Eq, Message)] struct FirstMessageType { @@ -707,7 +745,7 @@ mod tests { fn recorder_can_be_stopped_on_a_particular_message() { let system = System::new("recorder_can_be_stopped_on_a_particular_message"); let recorder = - Recorder::new().system_stop_conditions(match_every_type_id!(FirstMessageType)); + Recorder::new().system_stop_conditions(match_lazily_every_type_id!(FirstMessageType)); let recording_arc = recorder.get_recording(); let rec_addr: Addr = recorder.start(); @@ -747,25 +785,32 @@ mod tests { #[test] fn diff_counter_msgs_with_diff_id_methods_can_be_used() { let (respondent, _, respondent_recording_arc) = make_recorder(); - let respondent = respondent - .system_stop_conditions(match_every_type_id!(ScanForReceivables, NodeToUiMessage)); + let respondent = respondent.system_stop_conditions(match_lazily_every_type_id!( + ScanForReceivables, + NodeToUiMessage + )); let respondent_addr = respondent.start(); let msg_1 = StartMessage {}; let msg_1_type_id = msg_1.type_id(); let counter_msg_1 = ScanForReceivables { response_skeleton_opt: None, }; - let id_method_1 = MsgIdentification::ByType(msg_1.type_id()); - let setup_1 = - CounterMsgSetup::new(msg_1_type_id, id_method_1, counter_msg_1, &respondent_addr); + // Also testing this convenient macro if it works fine + let setup_1 = setup_for_counter_msg_triggered_via_type_id!( + StartMessage, + counter_msg_1, + &respondent_addr + ); let counter_msg_2_strayed = StartMessage {}; let random_id = TypeId::of::(); let id_method_2 = MsgIdentification::ByType(random_id); - let setup_2 = CounterMsgSetup::new( + let setup_2 = SingleCounterMsgSetup::new( random_id, id_method_2, - counter_msg_2_strayed, - &respondent_addr, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg_2_strayed, + respondent_addr.clone().recipient(), + ))], ); let msg_3_unmatching = NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(7, 7, 7, 7)), @@ -782,8 +827,14 @@ mod tests { let id_method_3 = MsgIdentification::ByMatch { exemplar: Box::new(NewPublicIp { new_ip: make_ip(1) }), }; - let setup_3 = - CounterMsgSetup::new(msg_3_type_id, id_method_3, counter_msg_3, &respondent_addr); + let setup_3 = SingleCounterMsgSetup::new( + msg_3_type_id, + id_method_3, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg_3, + respondent_addr.clone().recipient(), + ))], + ); let msg_4_matching = NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(4, 5, 6, 7)), }; @@ -799,21 +850,23 @@ mod tests { let id_method_4 = MsgIdentification::ByMatch { exemplar: Box::new(msg_4_matching.clone()), }; - let setup_4 = CounterMsgSetup::new( + let setup_4 = SingleCounterMsgSetup::new( msg_4_type_id, id_method_4, - counter_msg_4.clone(), - &respondent_addr, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg_4.clone(), + respondent_addr.clone().recipient(), + ))], ); let system = System::new("test"); let (subject, _, subject_recording_arc) = make_recorder(); - // Adding messages in a tangled manner - let subject = subject - .add_counter_msg(setup_3) - .add_counter_msg(setup_1) - .add_counter_msg(setup_2) - .add_counter_msg(setup_4); let subject_addr = subject.start(); + // Adding messages in a tangled manner + subject_addr + .try_send(SetUpCounterMsgs { + setups: vec![setup_3, setup_1, setup_2, setup_4], + }) + .unwrap(); subject_addr.try_send(msg_1).unwrap(); subject_addr.try_send(msg_3_unmatching.clone()).unwrap(); @@ -838,7 +891,7 @@ mod tests { fn counter_msgs_of_same_type_are_revisited_sequentially_and_triggered_by_first_matching_id_method( ) { let (respondent, _, respondent_recording_arc) = make_recorder(); - let respondent = respondent.system_stop_conditions(match_every_type_id!( + let respondent = respondent.system_stop_conditions(match_lazily_every_type_id!( ConfigChangeMsg, ConfigChangeMsg, ConfigChangeMsg @@ -849,7 +902,6 @@ mod tests { exit_code: None, stderr: Some("blah".to_string()), }; - let msg_1_type_id = msg_1.type_id(); let counter_msg_1 = ConfigChangeMsg { change: ConfigChange::UpdateMinHops(Hops::SixHops), }; @@ -859,8 +911,12 @@ mod tests { msg.process_id == 1010 }), }; - let setup_1 = - CounterMsgSetup::new(msg_1_type_id, id_method_1, counter_msg_1, &respondent_addr); + let setup_1 = setup_for_counter_msg_triggered_via_specific_msg_id_method!( + CrashNotification, + id_method_1, + counter_msg_1, + &respondent_addr + ); let msg_2 = CrashNotification { process_id: 1010, exit_code: Some(11), @@ -870,9 +926,11 @@ mod tests { let counter_msg_2 = ConfigChangeMsg { change: ConfigChange::UpdatePassword("betterPassword".to_string()), }; - let id_method_2 = MsgIdentification::ByType(msg_2.type_id()); - let setup_2 = - CounterMsgSetup::new(msg_2_type_id, id_method_2, counter_msg_2, &respondent_addr); + let setup_2 = setup_for_counter_msg_triggered_via_type_id!( + CrashNotification, + counter_msg_2, + &respondent_addr + ); let msg_3 = CrashNotification { process_id: 9999999, exit_code: None, @@ -886,16 +944,20 @@ mod tests { }), }; let id_method_3 = MsgIdentification::ByType(msg_3.type_id()); - let setup_3 = - CounterMsgSetup::new(msg_3_type_id, id_method_3, counter_msg_3, &respondent_addr); + let setup_3 = setup_for_counter_msg_triggered_via_type_id!( + CrashNotification, + counter_msg_3, + &respondent_addr + ); let system = System::new("test"); let (subject, _, subject_recording_arc) = make_recorder(); - // Adding messages in standard order - let subject = subject - .add_counter_msg(setup_1) - .add_counter_msg(setup_2) - .add_counter_msg(setup_3); let subject_addr = subject.start(); + // Adding messages in standard order + subject_addr + .try_send(SetUpCounterMsgs { + setups: vec![setup_1, setup_2, setup_3], + }) + .unwrap(); // More tricky scenario picked on purpose subject_addr.try_send(msg_3.clone()).unwrap(); diff --git a/node/src/test_utils/recorder_counter_msgs.rs b/node/src/test_utils/recorder_counter_msgs.rs index 11adeff67..1baa313c7 100644 --- a/node/src/test_utils/recorder_counter_msgs.rs +++ b/node/src/test_utils/recorder_counter_msgs.rs @@ -4,8 +4,8 @@ use crate::test_utils::recorder_stop_conditions::{ForcedMatchable, MsgIdentification}; use actix::dev::ToEnvelope; -use actix::{Addr, Handler, Message, Recipient}; -use std::any::{type_name, TypeId}; +use actix::{Actor, Addr, Handler, Message, Recipient}; +use std::any::TypeId; use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::HashMap; @@ -47,45 +47,36 @@ where } } -pub struct CounterMsgSetup { +pub struct SingleCounterMsgSetup { // Leave them private trigger_msg_type_id: TypeId, condition: MsgIdentification, - msg_gear: Box, + // Multiple messages sent off on reaction are allowed + // (Imagine a message handler whose execution goes about more than just one msg dispatch) + msg_gears: Vec>, } -impl CounterMsgSetup { - pub fn new( +impl SingleCounterMsgSetup { + pub fn new( trigger_msg_type_id: TypeId, trigger_msg_id_method: MsgIdentification, - counter_msg: Msg, - counter_msg_actor_addr: &Addr, - ) -> Self - where - Msg: Message + Send + 'static, - Msg::Result: Send, - Actor: actix::Actor + Handler, - Actor::Context: ToEnvelope, - { - let msg_gear = Box::new(SendableCounterMsgWithRecipient::new( - counter_msg, - counter_msg_actor_addr.clone().recipient(), - )); + counter_messages: Vec>, + ) -> Self { Self { trigger_msg_type_id, condition: trigger_msg_id_method, - msg_gear, + msg_gears: counter_messages, } } } #[derive(Default)] pub struct CounterMessages { - msgs: HashMap>, + msgs: HashMap>, } impl CounterMessages { - pub fn search_for_msg_setup(&mut self, msg: &Msg) -> Option> + pub fn search_for_msg_setup(&mut self, msg: &Msg) -> Option>> where Msg: ForcedMatchable + 'static, { @@ -96,14 +87,14 @@ impl CounterMessages { .position(|cm_setup| cm_setup.condition.resolve_condition(msg)) .map(|idx| { let matching_counter_msg = msgs_vec.remove(idx); - matching_counter_msg.msg_gear + matching_counter_msg.msg_gears }) } else { None } } - pub fn add_msg(&mut self, counter_msg_setup: CounterMsgSetup) { + pub fn add_msg(&mut self, counter_msg_setup: SingleCounterMsgSetup) { let type_id = counter_msg_setup.trigger_msg_type_id; match self.msgs.entry(type_id) { Entry::Occupied(mut existing) => existing.get_mut().push(counter_msg_setup), @@ -113,3 +104,41 @@ impl CounterMessages { } } } + +#[macro_export] +macro_rules! setup_for_counter_msg_triggered_via_type_id{ + ($trigger_msg_type: ty, $($owned_counter_msg: expr, $respondent_actor_addr_ref: expr),+) => { + + crate::setup_for_counter_msg_triggered_via_specific_msg_id_method!( + $trigger_msg_type, + MsgIdentification::ByType(TypeId::of::<$trigger_msg_type>()), + $($owned_counter_msg, $respondent_actor_addr_ref),+ + ) + }; +} + +#[macro_export] +macro_rules! setup_for_counter_msg_triggered_via_specific_msg_id_method{ + ($trigger_msg_type: ty, $msg_id_method: expr, $($owned_counter_msg: expr, $respondent_actor_addr_ref: expr),+) => { + // This macro returns a block of operations. That's why it begins with these curly brackets + { + let msg_gears: Vec< + Box + > = vec![ + // This part can be repeated as long as there are more expression pairs suplied + $(Box::new( + crate::test_utils::recorder_counter_msgs::SendableCounterMsgWithRecipient::new( + $owned_counter_msg, + $respondent_actor_addr_ref.clone().recipient() + ) + )),+ + ]; + + SingleCounterMsgSetup::new( + TypeId::of::<$trigger_msg_type>(), + $msg_id_method, + msg_gears + ) + } + }; +} diff --git a/node/src/test_utils/recorder_stop_conditions.rs b/node/src/test_utils/recorder_stop_conditions.rs index 1120b7e88..92fefe6e3 100644 --- a/node/src/test_utils/recorder_stop_conditions.rs +++ b/node/src/test_utils/recorder_stop_conditions.rs @@ -7,7 +7,12 @@ use std::any::{Any, TypeId}; pub enum StopConditions { Any(Vec), - All(Vec), + // Msg is tested against every ID method in the vector. In every case when they match, those + // methods are eliminated + AllGreedily(Vec), + // Iterating through the vector of ID methods, only the first matching method is eliminated + // and any other one that would've matched needs to wait for the next received msg of this type + AllLazily(Vec), } pub enum MsgIdentification { @@ -24,43 +29,48 @@ pub type BoxedMsgExpected = Box; pub type RefMsgExpected<'a> = &'a (dyn Any + Send); impl StopConditions { - pub fn resolve_stop_conditions + Send + 'static>( + pub fn resolve_stop_conditions + Send + 'static>( &mut self, - msg: &T, + msg: &Msg, ) -> bool { match self { - StopConditions::Any(conditions) => Self::resolve_any::(conditions, msg), - StopConditions::All(conditions) => Self::resolve_all::(conditions, msg), + StopConditions::Any(conditions) => Self::resolve_any::(conditions, msg), + StopConditions::AllGreedily(conditions) => { + Self::resolve_all_greedily::(conditions, msg) + } + StopConditions::AllLazily(conditions) => { + Self::resolve_all_lazily::(conditions, msg) + } } } - fn resolve_any + Send + 'static>( + fn resolve_any + Send + 'static>( conditions: &Vec, - msg: &T, + msg: &Msg, ) -> bool { conditions .iter() - .any(|condition| condition.resolve_condition::(msg)) + .any(|condition| condition.resolve_condition::(msg)) } - fn resolve_all + Send + 'static>( + fn resolve_all_greedily + Send + 'static>( conditions: &mut Vec, - msg: &T, + msg: &Msg, ) -> bool { let indexes_to_remove = Self::indexes_of_matched_conditions(conditions, msg); Self::remove_matched_conditions(conditions, indexes_to_remove); conditions.is_empty() } - fn indexes_of_matched_conditions + Send + 'static>( + fn indexes_of_matched_conditions + Send + 'static>( conditions: &[MsgIdentification], - msg: &T, + msg: &Msg, ) -> Vec { conditions .iter() .enumerate() .fold(vec![], |mut acc, (idx, condition)| { - let matches = condition.resolve_condition::(msg); + let matches = condition.resolve_condition::(msg); if matches { acc.push(idx) } @@ -68,6 +78,19 @@ impl StopConditions { }) } + fn resolve_all_lazily + Send + 'static>( + conditions: &mut Vec, + msg: &Msg, + ) -> bool { + if let Some(idx) = conditions + .iter() + .position(|condition| condition.resolve_condition::(msg)) + { + conditions.remove(idx); + } + conditions.is_empty() + } + fn remove_matched_conditions( conditions: &mut Vec, indexes_to_remove: Vec, @@ -148,9 +171,9 @@ impl PartialEq for PretendedMatchableWrapper { } #[macro_export] -macro_rules! match_every_type_id{ +macro_rules! match_lazily_every_type_id{ ($($single_message: ident),+) => { - StopConditions::All(vec![$(MsgIdentification::ByType(TypeId::of::<$single_message>())),+]) + StopConditions::AllLazily(vec![$(MsgIdentification::ByType(TypeId::of::<$single_message>())),+]) } } @@ -161,7 +184,7 @@ mod tests { use crate::sub_lib::peer_actors::{NewPublicIp, StartMessage}; use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; use std::any::TypeId; - use std::net::{IpAddr, Ipv4Addr}; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::vec; #[test] @@ -186,15 +209,15 @@ mod tests { #[test] fn stop_on_match_works() { - let mut cond1 = StopConditions::All(vec![MsgIdentification::ByMatch { + let mut cond1 = StopConditions::AllGreedily(vec![MsgIdentification::ByMatch { exemplar: Box::new(StartMessage {}), }]); - let mut cond2 = StopConditions::All(vec![MsgIdentification::ByMatch { + let mut cond2 = StopConditions::AllGreedily(vec![MsgIdentification::ByMatch { exemplar: Box::new(NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(1, 8, 6, 4)), }), }]); - let mut cond3 = StopConditions::All(vec![MsgIdentification::ByMatch { + let mut cond3 = StopConditions::AllGreedily(vec![MsgIdentification::ByMatch { exemplar: Box::new(NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(44, 2, 3, 1)), }), @@ -219,7 +242,7 @@ mod tests { #[test] fn stop_on_predicate_works() { - let mut cond_set = StopConditions::All(vec![MsgIdentification::ByPredicate { + let mut cond_set = StopConditions::AllGreedily(vec![MsgIdentification::ByPredicate { predicate: Box::new(|msg| { let scan_err_msg: &ScanError = msg.downcast_ref().unwrap(); scan_err_msg.scan_type == ScanType::PendingPayables @@ -265,7 +288,12 @@ mod tests { }; let inspect_len_of_any = |cond_set: &StopConditions, msg_number: usize| match cond_set { StopConditions::Any(conditions) => conditions.len(), - StopConditions::All(_) => panic!("stage {}: expected Any but got All", msg_number), + StopConditions::AllGreedily(_) => { + panic!("stage {}: expected Any but got AllGreedily", msg_number) + } + StopConditions::AllLazily(_) => { + panic!("stage {}: expected Any but got AllLazily", msg_number) + } }; assert_eq!( @@ -289,8 +317,8 @@ mod tests { } #[test] - fn match_all_with_conditions_gradually_eliminated_until_vector_is_emptied_and_it_is_match() { - let mut cond_set = StopConditions::All(vec![ + fn match_all_with_conditions_gradually_eliminated_greedily_until_empty() { + let mut cond_set = StopConditions::AllGreedily(vec![ MsgIdentification::ByPredicate { predicate: Box::new(|msg| { if let Some(ip_msg) = msg.downcast_ref::() { @@ -321,12 +349,17 @@ mod tests { assert_eq!(kill_system, false); match &cond_set { - StopConditions::All(conds) => { + StopConditions::AllGreedily(conds) => { assert_eq!(conds.len(), 2); assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); assert!(matches!(conds[1], MsgIdentification::ByType(_))); } - StopConditions::Any(_) => panic!("Stage 1: expected StopConditions::All, not ...Any"), + StopConditions::Any(_) => { + panic!("Stage 1: expected StopConditions::AllGreedily, not Any") + } + StopConditions::AllLazily(_) => { + panic!("Stage 1: expected StopConditions::AllGreedily, not AllLazily") + } } let tested_msg_2 = NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(1, 2, 4, 1)), @@ -336,10 +369,96 @@ mod tests { assert_eq!(kill_system, true); match cond_set { - StopConditions::All(conds) => { + StopConditions::AllGreedily(conds) => { assert!(conds.is_empty()) } - StopConditions::Any(_) => panic!("Stage 2: expected StopConditions::All, not ...Any"), + StopConditions::Any(_) => { + panic!("Stage 2: expected StopConditions::AllGreedily, not Any") + } + StopConditions::AllLazily(_) => { + panic!("Stage 2: expected StopConditions::AllGreedily, not AllLazily") + } + } + } + + #[test] + fn match_all_with_conditions_gradually_eliminated_lazily_until_empty() { + let mut cond_set = StopConditions::AllLazily(vec![ + MsgIdentification::ByPredicate { + predicate: Box::new(|msg| { + if let Some(ip_msg) = msg.downcast_ref::() { + ip_msg.new_ip.is_ipv6() + } else { + false + } + }), + }, + MsgIdentification::ByType(TypeId::of::()), + MsgIdentification::ByType(TypeId::of::()), + ]); + let tested_msg_1 = ScanForNewPayables { + response_skeleton_opt: None, + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_1); + + assert_eq!(kill_system, false); + assert_on_state_after_lazily_matched(1, &cond_set, |conds| { + assert_eq!(conds.len(), 3); + assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); + assert!(matches!(conds[1], MsgIdentification::ByType(_))); + assert!(matches!(conds[2], MsgIdentification::ByType(_))); + }); + let tested_msg_2 = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(6, 7, 8, 9)), + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_2); + + assert_eq!(kill_system, false); + assert_on_state_after_lazily_matched(2, &cond_set, |conds| { + assert_eq!(conds.len(), 2); + assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); + assert!(matches!(conds[1], MsgIdentification::ByType(_))); + }); + let tested_msg_3 = NewPublicIp { + new_ip: IpAddr::V6(Ipv6Addr::new(1, 2, 4, 1, 4, 3, 2, 1)), + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_3); + + assert_eq!(kill_system, false); + assert_on_state_after_lazily_matched(3, &cond_set, |conds| { + assert_eq!(conds.len(), 1); + assert!(matches!(conds[0], MsgIdentification::ByType(_))) + }); + let tested_msg_4 = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(45, 45, 45, 45)), + }; + + let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_4); + + assert_eq!(kill_system, true); + assert_on_state_after_lazily_matched(4, &cond_set, |conds| { + assert!(conds.is_empty()); + }); + } + + fn assert_on_state_after_lazily_matched( + stage: usize, + cond_set: &StopConditions, + assertions: fn(&[MsgIdentification]), + ) { + match &cond_set { + StopConditions::AllLazily(conds) => assertions(conds), + StopConditions::Any(_) => panic!( + "Stage {}: expected StopConditions::AllLazily, not Any", + stage + ), + StopConditions::AllGreedily(_) => panic!( + "Stage {}: expected StopConditions::AllLazily, not AllGreedily", + stage + ), } } } From 0968f41e491b827a36a1c1bbf0e0000bab7a3de1 Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 27 Apr 2025 16:02:30 +0200 Subject: [PATCH 09/49] GH-602: main part probably done --- node/src/accountant/mod.rs | 48 +++++----------- node/src/accountant/scanners/mod.rs | 14 ++--- node/src/accountant/scanners/test_utils.rs | 1 - node/src/test_utils/mod.rs | 60 +++++++++++--------- node/src/test_utils/recorder.rs | 4 -- node/src/test_utils/recorder_counter_msgs.rs | 3 +- 6 files changed, 52 insertions(+), 78 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index e60fdf03d..5f54b002f 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -258,7 +258,6 @@ impl Handler for Accountant { fn handle(&mut self, msg: ReportTransactionReceipts, ctx: &mut Self::Context) -> Self::Result { let response_skeleton_opt = msg.response_skeleton_opt; - match self.scanners.pending_payable.finish_scan(msg, &self.logger) { UiScanResult::Finished(ui_msg_opt) => { if let Some(node_to_ui_msg) = ui_msg_opt { @@ -308,7 +307,7 @@ impl Handler for Accountant { .try_send(node_to_ui_msg) .expect("UIGateway is dead"); } - + self.scan_schedulers.pending_payable.schedule(ctx, None) } } @@ -639,19 +638,6 @@ impl Accountant { } } - // fn schedule_next_scan( - // &self, - // scan_type: ScanType, - // ctx: &mut Context, - // response_skeleton_opt: Option, - // ) { - // self.scan_schedulers - // .schedulers - // .get(&scan_type) - // .unwrap_or_else(|| panic!("Scan Scheduler {:?} not properly prepared", scan_type)) - // .schedule(ctx, response_skeleton_opt) - // } - fn handle_report_routing_service_provided_message( &mut self, msg: ReportRoutingServiceProvidedMessage, @@ -1145,7 +1131,7 @@ mod tests { use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::payable_scanner::test_utils::BlockchainAgentMock; use crate::accountant::scanners::test_utils::{NewPayableScanDynIntervalComputerMock}; - use crate::accountant::scanners::{AccessibleScanner, BeginScanError, PayableScanner}; + use crate::accountant::scanners::{BeginScanError}; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; @@ -1160,7 +1146,7 @@ mod tests { use crate::database::test_utils::transaction_wrapper_mock::TransactionInnerWrapperMockBuilder; use crate::db_config::config_dao::ConfigDaoRecord; use crate::db_config::mocks::ConfigDaoMock; - use crate::{match_lazily_every_type_id, setup_for_counter_msg_triggered_via_type_id, setup_for_counter_msg_triggered_via_specific_msg_id_method}; + use crate::{match_lazily_every_type_id, setup_for_counter_msg_triggered_via_type_id}; use crate::sub_lib::accountant::{ ExitServiceConsumed, PaymentThresholds, RoutingServiceConsumed, ScanIntervals, DEFAULT_EARNING_WALLET, DEFAULT_PAYMENT_THRESHOLDS, @@ -2308,18 +2294,13 @@ mod tests { }), }; let pending_payable_fingerprint = make_pending_payable_fingerprint(); - let counter_msg_4 = ReportTransactionReceipts { + let counter_msg_3 = ReportTransactionReceipts { fingerprints_with_receipts: vec![( TransactionReceiptResult::RpcResponse(tx_receipt), pending_payable_fingerprint.clone(), )], response_skeleton_opt: None, }; - let blockchain_bridge = - blockchain_bridge.system_stop_conditions(match_lazily_every_type_id!( - QualifiedPayablesMessage, - RequestTransactionReceipts //RequestTransactionReceipts - )); let blockchain_bridge_addr = blockchain_bridge.start(); let request_transaction_receipts = RequestTransactionReceipts { pending_payable: vec![pending_payable_fingerprint], @@ -2329,7 +2310,7 @@ mod tests { .started_at_result(None) .start_scan_params(&start_scan_pending_payable_params_arc) .start_scan_result(Ok(request_transaction_receipts.clone())) - .start_scan_result(Err(BeginScanError::NothingToProcess)); + .finish_scan_result(UiScanResult::Finished(None)); let qualified_payables_msg = QualifiedPayablesMessage { qualified_payables: qualified_payable.clone(), consuming_wallet: consuming_wallet.clone(), @@ -2337,11 +2318,10 @@ mod tests { }; let payable_scanner = ScannerMock::new() .started_at_result(None) - // Checking on both scanners within ScanForRetryPayable + // Always checking also on the payable scanner when handling ScanForPendingPayable .started_at_result(None) .start_scan_params(&start_scan_payable_params_arc) .start_scan_result(Ok(qualified_payables_msg.clone())) - .start_scan_result(Err(BeginScanError::NothingToProcess)) .finish_scan_result(UiScanResult::Finished(None)); let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { @@ -2351,7 +2331,6 @@ mod tests { pending_payable_scan_interval: Duration::from_millis(50), receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner }); - let pending_payable = PendingPayable::new(make_wallet("abc"), make_tx_hash(123)); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) .consuming_wallet(consuming_wallet.clone()) @@ -2368,7 +2347,8 @@ mod tests { subject.scan_schedulers.payable.new_payable_notify = Box::new( NotifyHandleMock::::default() .notify_params(¬ify_payable_params_arc) - .capture_msg_and_let_it_fly_on(), + // This should stop the system. If anything goes wrong, the SystemKillerActor will. + .stop_system_on_count_received(1) ); subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.clone().recipient()); subject.outbound_payments_instructions_sub_opt = @@ -2376,7 +2356,6 @@ mod tests { subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.clone().recipient()); let subject_addr = subject.start(); - let subject_subs = Accountant::make_subs_from(&subject_addr); let set_up_counter_msgs = SetUpCounterMsgs::new(vec![ setup_for_counter_msg_triggered_via_type_id!( QualifiedPayablesMessage, @@ -2388,6 +2367,11 @@ mod tests { counter_msg_2, &subject_addr ), + setup_for_counter_msg_triggered_via_type_id!( + RequestTransactionReceipts, + counter_msg_3, + &subject_addr + ) ]); blockchain_bridge_addr .try_send(set_up_counter_msgs) @@ -2457,12 +2441,6 @@ mod tests { tlh.exists_log_containing(&format!( "DEBUG: {test_name}: verifying pending payable scanner logger" )); - tlh.assert_logs_contain_in_order(vec![ - &format!( - "DEBUG: {test_name}: There was nothing to process during PendingPayables scan." - ), - &format!("DEBUG: {test_name}: There was nothing to process during Payables scan."), - ]); } #[test] diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 6ef74c399..d29ac4633 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -48,7 +48,6 @@ use std::time::{SystemTime}; use time::format_description::parse; use time::OffsetDateTime; use web3::types::H256; -use crate::accountant::scanners::local_test_utils::NullScanner; use crate::accountant::scanners::payable_scanner::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; use crate::accountant::scanners::payable_scanner::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; @@ -821,12 +820,9 @@ impl AccessibleScanner }; match message.fingerprints_with_receipts.is_empty() { - // TODO changed to error log, find a place where it's gonna be tested true => { - error!(logger, "No transaction receipts found."); - todo!("see if the test assert on the mark_as_ended"); - self.mark_as_ended(logger); - UiScanResult::Finished(construct_msg_scan_ended_to_ui()) + debug!(logger, "No transaction receipts found."); + todo!("requires payments retry"); } false => { debug!( @@ -838,6 +834,8 @@ impl AccessibleScanner let requires_payments_retry = self.process_transactions_by_reported_state(scan_report, logger); + self.mark_as_ended(&logger); + if requires_payments_retry { todo!() } else { @@ -1283,7 +1281,7 @@ pub mod local_test_utils { use crate::accountant::scanners::payable_scanner::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::{ AccessibleScanner, BeginScanError, InaccessibleScanner, MultistageDualPayableScanner, - PreparedAdjustment, PrivateScanStarter, ScanWithStarter, Scanners, + PreparedAdjustment, PrivateScanStarter, ScanWithStarter, SolvencySensitivePaymentInstructor, StartableAccessibleScanner, UiScanResult, }; use crate::accountant::BlockchainAgentWithContextMessage; @@ -3648,7 +3646,7 @@ mod tests { assert_eq!(is_scan_running, false); let tlh = TestLogHandler::new(); tlh.exists_log_containing(&format!( - "DEBUG: {test_name}: No transaction receipts found." + "ERROR: {test_name}: No transaction receipts found." )); tlh.exists_log_matching(&format!( "INFO: {test_name}: The PendingPayables scan ended in \\d+ms." diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 90d16f491..bc5cb0076 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -2,7 +2,6 @@ #![cfg(test)] -use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::scanners::scan_schedulers::NewPayableScanDynIntervalComputer; use std::cell::RefCell; use std::sync::{Arc, Mutex}; diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index d04546aa4..512ba9ea3 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -875,7 +875,7 @@ pub mod unshared_test_utils { pub struct NotifyLaterHandleMock { notify_later_params: Arc>>, - stop_system_after_call_num_opt: RefCell>, + stop_system_on_count_received_opt: RefCell>, send_message_out: bool, } @@ -883,7 +883,7 @@ pub mod unshared_test_utils { fn default() -> Self { Self { notify_later_params: Arc::new(Mutex::new(vec![])), - stop_system_after_call_num_opt: RefCell::new(None), + stop_system_on_count_received_opt: RefCell::new(None), send_message_out: false, } } @@ -895,11 +895,13 @@ pub mod unshared_test_utils { self } - pub fn stop_system_after_call_count(self, count: usize) -> Self { + pub fn stop_system_on_count_received(self, count: usize) -> Self { if count == 0 { - panic!("Should be considered starting from 1") + panic!("Should be a none-zero value") } - self.stop_system_after_call_num_opt.replace(Some(count)); + let system_killer = SystemKillerActor::new(Duration::from_secs(10)); + system_killer.start(); + self.stop_system_on_count_received_opt.replace(Some(count)); self } @@ -907,28 +909,6 @@ pub mod unshared_test_utils { self.send_message_out = true; self } - - fn should_stop_the_system(&self) { - let stop = if let Some(allowed_calls) = - self.stop_system_after_call_num_opt.borrow().as_ref() - { - *allowed_calls == 1 - } else { - return; - }; - if stop { - System::current().stop() - } else { - let allowed_calls = *self - .stop_system_after_call_num_opt - .borrow() - .as_ref() - .unwrap(); - let _ = self - .stop_system_after_call_num_opt - .replace(Some(allowed_calls - 1)); - } - } } impl NotifyLaterHandle for NotifyLaterHandleMock @@ -946,7 +926,12 @@ pub mod unshared_test_utils { .lock() .unwrap() .push((msg.clone(), interval)); - self.should_stop_the_system(); + if let Some(remaining) = self.stop_system_on_count_received_opt.borrow_mut().as_mut() { + if remaining == &0 { + System::current().stop(); + } + *remaining -= 1; + } if self.send_message_out { let handle = ctx.notify_later(msg, interval); Box::new(NLSpawnHandleHolderReal::new(handle)) @@ -967,6 +952,7 @@ pub mod unshared_test_utils { pub struct NotifyHandleMock { notify_params: Arc>>, send_message_out: bool, + stop_system_on_count_received_opt: RefCell> } impl Default for NotifyHandleMock { @@ -974,6 +960,7 @@ pub mod unshared_test_utils { Self { notify_params: Arc::new(Mutex::new(vec![])), send_message_out: false, + stop_system_on_count_received_opt: RefCell::new(None), } } } @@ -988,6 +975,16 @@ pub mod unshared_test_utils { self.send_message_out = true; self } + + pub fn stop_system_on_count_received(self, msg_count: usize)-> Self { + if msg_count == 0 { + panic!("Should be a non-zero value") + } + let system_killer = SystemKillerActor::new(Duration::from_secs(10)); + system_killer.start(); + self.stop_system_on_count_received_opt.replace(Some(msg_count)); + self + } } impl NotifyHandle for NotifyHandleMock @@ -997,6 +994,13 @@ pub mod unshared_test_utils { { fn notify<'a>(&'a self, msg: M, ctx: &'a mut Context) { self.notify_params.lock().unwrap().push(msg.clone()); + if let Some(remaining) = self.stop_system_on_count_received_opt.borrow_mut().as_mut() { + if remaining == &0 { + System::current().stop(); + return; + } + *remaining -= 1; + } if self.send_message_out { ctx.notify(msg) } diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index a559b6546..b11fce618 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -791,7 +791,6 @@ mod tests { )); let respondent_addr = respondent.start(); let msg_1 = StartMessage {}; - let msg_1_type_id = msg_1.type_id(); let counter_msg_1 = ScanForReceivables { response_skeleton_opt: None, }; @@ -922,7 +921,6 @@ mod tests { exit_code: Some(11), stderr: None, }; - let msg_2_type_id = msg_2.type_id(); let counter_msg_2 = ConfigChangeMsg { change: ConfigChange::UpdatePassword("betterPassword".to_string()), }; @@ -936,14 +934,12 @@ mod tests { exit_code: None, stderr: None, }; - let msg_3_type_id = msg_3.type_id(); let counter_msg_3 = ConfigChangeMsg { change: ConfigChange::UpdateWallets(WalletPair { consuming_wallet: make_wallet("abc"), earning_wallet: make_wallet("def"), }), }; - let id_method_3 = MsgIdentification::ByType(msg_3.type_id()); let setup_3 = setup_for_counter_msg_triggered_via_type_id!( CrashNotification, counter_msg_3, diff --git a/node/src/test_utils/recorder_counter_msgs.rs b/node/src/test_utils/recorder_counter_msgs.rs index 1baa313c7..9a8997724 100644 --- a/node/src/test_utils/recorder_counter_msgs.rs +++ b/node/src/test_utils/recorder_counter_msgs.rs @@ -3,8 +3,7 @@ #![cfg(test)] use crate::test_utils::recorder_stop_conditions::{ForcedMatchable, MsgIdentification}; -use actix::dev::ToEnvelope; -use actix::{Actor, Addr, Handler, Message, Recipient}; +use actix::{Message, Recipient}; use std::any::TypeId; use std::cell::RefCell; use std::collections::hash_map::Entry; From 69b4dd01fed0ae46cc1292b6a16c816b6bdfdd84 Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 27 Apr 2025 22:10:37 +0200 Subject: [PATCH 10/49] GH-602: more todos knocked off in accountant/mod.rs --- node/src/accountant/mod.rs | 304 +++++++++++++++++----------- node/src/accountant/scanners/mod.rs | 19 +- node/src/test_utils/mod.rs | 21 +- 3 files changed, 214 insertions(+), 130 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index d54671268..0ceb67967 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -269,12 +269,10 @@ impl Handler for Accountant { } self.scan_schedulers.payable.schedule_for_new_payable(ctx) } - UiScanResult::MultipleScanSequenceUnfinished => { - todo!(); - self.scan_schedulers - .payable - .schedule_for_retry_payable(ctx, response_skeleton_opt) - } + UiScanResult::ChainedScannersUnfinished => self + .scan_schedulers + .payable + .schedule_for_retry_payable(ctx, response_skeleton_opt), }; } } @@ -903,36 +901,32 @@ impl Accountant { &mut self, response_skeleton_opt: Option, ) { - todo!( - "This must be written in a different card. The new start_scan method for \ - the payable scanner must be filled with code first - the SentPayableDao with its new \ - method allowing to filter out payments that need to be retried" - ) - // let result: Result = - // match self.consuming_wallet_opt.as_ref() { - // Some(consuming_wallet) => self.scanners.start_retry_payable_scan_guarded( - // consuming_wallet, - // SystemTime::now(), - // response_skeleton_opt, - // &self.logger, - // ), - // None => Err(BeginScanError::NoConsumingWalletFound), - // }; - // - // match result { - // Ok(scan_message) => { - // self.qualified_payables_sub_opt - // .as_ref() - // .expect("BlockchainBridge is unbound") - // .try_send(scan_message) - // .expect("BlockchainBridge is dead"); - // } - // Err(e) => e.handle_error( - // &self.logger, - // ScanType::Payables, - // response_skeleton_opt.is_some(), - // ), - // } + let result: Result = + match self.consuming_wallet_opt.as_ref() { + Some(consuming_wallet) => self.scanners.start_retry_payable_scan_guarded( + consuming_wallet, + SystemTime::now(), + response_skeleton_opt, + &self.logger, + ), + None => todo!(), //Err(BeginScanError::NoConsumingWalletFound), + }; + + match result { + Ok(scan_message) => { + self.qualified_payables_sub_opt + .as_ref() + .expect("BlockchainBridge is unbound") + .try_send(scan_message) + .expect("BlockchainBridge is dead"); + } + Err(e) => todo!() + // e.handle_error( + // &self.logger, + // ScanType::Payables, + // response_skeleton_opt.is_some(), + //), + } } fn handle_request_of_scan_for_pending_payable( @@ -967,12 +961,7 @@ impl Accountant { response_skeleton_opt.is_some(), ); - //TODO rewrite into simple "e==BeginScanError::NothingToProcess" after testing - if e == BeginScanError::NothingToProcess { - true - } else { - false - } + e == BeginScanError::NothingToProcess } } } @@ -1012,6 +1001,7 @@ impl Accountant { ) { match scan_type { CommendableScanType::Payables => { + todo!(); if self.handle_request_of_scan_for_pending_payable(Some(response_skeleton)) { todo!() } @@ -1168,7 +1158,7 @@ mod tests { use actix::{System}; use ethereum_types::U64; use ethsign_crypto::Keccak256; - use log::Level; + use log::{logger, Level}; use masq_lib::constants::{ REQUEST_WITH_MUTUALLY_EXCLUSIVE_PARAMS, REQUEST_WITH_NO_VALUES, SCAN_ERROR, VALUE_EXCEEDS_ALLOWED_LIMIT, @@ -1487,7 +1477,58 @@ mod tests { } #[test] - fn scan_payables_request() { + fn external_scan_payables_request() { + let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); + config.suppress_initial_scans = true; + let fingerprint = PendingPayableFingerprint { + rowid: 1234, + timestamp: SystemTime::now(), + hash: Default::default(), + attempt: 1, + amount: 1_000_000, + process_error: None, + }; + let pending_payable_dao = PendingPayableDaoMock::default() + .return_all_errorless_fingerprints_result(vec![fingerprint.clone()]); + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_paying_wallet(b"consuming")) + .bootstrapper_config(config) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge = blockchain_bridge + .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)); + let blockchain_bridge_addr = blockchain_bridge.start(); + subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); + let subject_addr = subject.start(); + let system = System::new("test"); + let ui_message = NodeFromUiMessage { + client_id: 1234, + body: UiScanRequest { + scan_type: CommendableScanType::Payables, + } + .tmb(4321), + }; + + subject_addr.try_send(ui_message).unwrap(); + + system.run(); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + assert_eq!( + blockchain_bridge_recording.get_record::(0), + &RequestTransactionReceipts { + pending_payable: vec![fingerprint], + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 1234, + context_id: 4321, + }), + } + ); + } + + #[test] + fn external_scan_payables_request_does_not_schedule_scan_for_new_payables_if_payables_empty() { + todo!("write me up"); let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); config.suppress_initial_scans = true; let fingerprint = PendingPayableFingerprint { @@ -1841,6 +1882,7 @@ mod tests { // a response skeleton #[test] fn report_transaction_receipts_with_response_skeleton_sends_scan_response_to_ui_gateway() { + todo!("fix me"); let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), @@ -1939,6 +1981,69 @@ mod tests { ); } + #[test] + fn accountant_handles_scan_for_retry_payables() { + init_test_logging(); + let test_name = "accountant_handles_scan_for_retry_payables"; + let start_scan_params_arc = Arc::new(Mutex::new(vec![])); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let system = System::new(test_name); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + let consuming_wallet = make_wallet("abc"); + subject.consuming_wallet_opt = Some(consuming_wallet.clone()); + let qualified_payables_msg = QualifiedPayablesMessage { + qualified_payables: vec![make_payable_account(789)], + consuming_wallet: consuming_wallet.clone(), + response_skeleton_opt: None, + }; + let payable_scanner_mock = ScannerMock::new() + .started_at_result(None) + .start_scan_params(&start_scan_params_arc) + .start_scan_result(Ok(qualified_payables_msg.clone())); + subject.scanners.payable = Box::new(payable_scanner_mock); + subject.scanners.pending_payable = Box::new(NullScanner::new()); + subject.scanners.receivable = Box::new(NullScanner::new()); + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 789, + context_id: 111, + }); + let accountant_addr = subject.start(); + let accountant_subs = Accountant::make_subs_from(&accountant_addr); + let peer_actors = peer_actors_builder() + .blockchain_bridge(blockchain_bridge) + .build(); + send_bind_message!(accountant_subs, peer_actors); + + accountant_addr + .try_send(ScanForRetryPayables { + response_skeleton_opt, + }) + .unwrap(); + + System::current().stop(); + let before = SystemTime::now(); + system.run(); + let after = SystemTime::now(); + let mut start_scan_params = start_scan_params_arc.lock().unwrap(); + let (actual_wallet, actual_now, actual_response_skeleton_opt, actual_logger) = + start_scan_params.remove(0); + assert_eq!(actual_wallet, consuming_wallet); + assert_eq!(actual_response_skeleton_opt, response_skeleton_opt); + assert!(before <= actual_now && actual_now <= after); + assert!( + start_scan_params.is_empty(), + "should be empty but was {:?}", + start_scan_params + ); + let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); + let message = blockchain_bridge_recorder.get_record::(0); + assert_eq!(message, &qualified_payables_msg); + assert_eq!(blockchain_bridge_recorder.len(), 1); + test_use_of_the_same_logger(&actual_logger, test_name) + } + #[test] fn accountant_requests_blockchain_bridge_to_scan_for_received_payments() { init_test_logging(); @@ -2347,7 +2452,7 @@ mod tests { NotifyHandleMock::::default() .notify_params(¬ify_payable_params_arc) // This should stop the system. If anything goes wrong, the SystemKillerActor will. - .stop_system_on_count_received(1) + .stop_system_on_count_received(1), ); subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.clone().recipient()); subject.outbound_payments_instructions_sub_opt = @@ -2370,7 +2475,7 @@ mod tests { RequestTransactionReceipts, counter_msg_3, &subject_addr - ) + ), ]); blockchain_bridge_addr .try_send(set_up_counter_msgs) @@ -3446,7 +3551,8 @@ mod tests { let payable_dao = PayableDaoMock::new() .mark_pending_payables_rowids_params(&mark_pending_payables_rowids_params_arc) .mark_pending_payables_rowids_result(Ok(())); - let system = System::new("accountant_processes_sent_payables_and_schedules_pending_payable_scanner"); + let system = + System::new("accountant_processes_sent_payables_and_schedules_pending_payable_scanner"); let mut subject = AccountantBuilder::default() .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) .payable_daos(vec![ForPayableScanner(payable_dao)]) @@ -3491,83 +3597,46 @@ mod tests { } #[test] - fn accountant_schedule_another_retry_payable_scanner_because_some_pending_payables_did_not_complete( - ) { - let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); - let payable_dao = PayableDaoMock::default() - .transactions_confirmed_params(&transactions_confirmed_params_arc) - .transactions_confirmed_result(Ok(())); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); - let mut subject = AccountantBuilder::default() - .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) - .build(); - let system = System::new("some_pending_payables_did_not_complete"); - let pending_payable_interval = Duration::from_millis(12); - subject.scan_schedulers.pending_payable.interval = pending_payable_interval; - subject.scan_schedulers.pending_payable.handle = Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(&pending_payable_notify_later_params_arc), - ); - let (msg, two_fingerprints) = make_report_transaction_receipts_msg( - TxStatus::Succeeded(TransactionBlock { - block_hash: Default::default(), - block_number: U64::from(100), - }), - TxStatus::Failed, - ); - let subject_addr = subject.start(); - - subject_addr.try_send(msg).unwrap(); - - System::current().stop(); - system.run(); - let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - assert_eq!( - *transactions_confirmed_params, - vec![vec![two_fingerprints[1].clone()]] - ); - let pending_payable_notify_later_params = - pending_payable_notify_later_params_arc.lock().unwrap(); - assert_eq!( - *pending_payable_notify_later_params, - vec![(ScanForPendingPayables::default(), pending_payable_interval)] - ); - } - - #[test] - fn accountant_schedule_another_retry_payable_scanner_because_all_pending_payables_still_pending( - ) { - let pending_payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_dao = - PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + fn accountant_schedule_retry_payable_scanner_because_not_all_pending_payables_completed() { + init_test_logging(); + let test_name = + "accountant_schedule_retry_payable_scanner_because_not_all_pending_payables_completed"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let retry_payable_notify_params_arc = Arc::new(Mutex::new(vec![])); let mut subject = AccountantBuilder::default() - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .logger(Logger::new(test_name)) .build(); - let system = System::new("all_pending_payables_still_pending"); - let pending_payable_interval = Duration::from_millis(23); - subject.scan_schedulers.pending_payable.interval = pending_payable_interval; - subject.scan_schedulers.pending_payable.handle = Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(&pending_payable_notify_later_params_arc), - ); - let (msg, _) = make_report_transaction_receipts_msg(TxStatus::Pending, TxStatus::Pending); + let pending_payable_scanner = ScannerMock::new() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(UiScanResult::ChainedScannersUnfinished); + subject.scanners.pending_payable = Box::new(pending_payable_scanner); + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); + let system = System::new(test_name); + let (mut msg, _) = + make_report_transaction_receipts_msg(TxStatus::Pending, TxStatus::Failed); + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 45, + context_id: 7, + }); + msg.response_skeleton_opt = response_skeleton_opt; let subject_addr = subject.start(); - subject_addr.try_send(msg).unwrap(); + subject_addr.try_send(msg.clone()).unwrap(); System::current().stop(); system.run(); - let pending_payable_notify_later_params = - pending_payable_notify_later_params_arc.lock().unwrap(); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (msg_actual, logger) = finish_scan_params.remove(0); + assert_eq!(msg_actual, msg); + let retry_payable_notify_params = retry_payable_notify_params_arc.lock().unwrap(); assert_eq!( - *pending_payable_notify_later_params, - vec![(ScanForPendingPayables::default(), pending_payable_interval)] + *retry_payable_notify_params, + vec![ScanForRetryPayables { + response_skeleton_opt + }] ); - // The payable DAO wasn't prepared in this test and therefore any payment confirmation did - // not take place + test_use_of_the_same_logger(&logger, test_name) } #[test] @@ -3719,13 +3788,12 @@ mod tests { } #[test] - fn scheduler_for_new_payables_computes_with_appropriate_now_timestamp() { + fn scheduler_for_new_payables_operates_with_proper_now_timestamp() { let new_payable_notify_later_arc = Arc::new(Mutex::new(vec![])); let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); let pending_payable_dao = PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); - let system = - System::new("scheduler_for_new_payables_computes_with_appropriate_now_timestamp"); + let system = System::new("scheduler_for_new_payables_operates_with_proper_now_timestamp"); let mut subject = AccountantBuilder::default() .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 8e5626452..9b971f93e 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -253,7 +253,7 @@ where #[derive(Debug, PartialEq)] pub enum UiScanResult { Finished(Option), - MultipleScanSequenceUnfinished, + ChainedScannersUnfinished, } impl UiScanResult { @@ -1391,7 +1391,7 @@ pub mod local_test_utils { pub struct ScannerMock { start_scan_params: Arc, Logger)>>>, start_scan_results: RefCell>>, - finish_scan_params: Arc>>, + finish_scan_params: Arc>>, finish_scan_results: RefCell>, started_at_results: RefCell>>, stop_system_after_last_message: RefCell, @@ -1452,8 +1452,11 @@ pub mod local_test_utils { StartMessage: Message, EndMessage: Message, { - fn finish_scan(&mut self, message: EndMessage, _logger: &Logger) -> UiScanResult { - self.finish_scan_params.lock().unwrap().push(message); + fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> UiScanResult { + self.finish_scan_params + .lock() + .unwrap() + .push((message, logger.clone())); if self.is_allowed_to_stop_the_system() && self.is_last_message() { System::current().stop(); } @@ -1509,6 +1512,14 @@ pub mod local_test_utils { self } + pub fn finish_scan_params( + mut self, + params: &Arc>>, + ) -> Self { + self.finish_scan_params = params.clone(); + self + } + pub fn finish_scan_result(self, result: UiScanResult) -> Self { self.finish_scan_results.borrow_mut().push(result); self diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 512ba9ea3..1221b7acb 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -926,7 +926,9 @@ pub mod unshared_test_utils { .lock() .unwrap() .push((msg.clone(), interval)); - if let Some(remaining) = self.stop_system_on_count_received_opt.borrow_mut().as_mut() { + if let Some(remaining) = + self.stop_system_on_count_received_opt.borrow_mut().as_mut() + { if remaining == &0 { System::current().stop(); } @@ -952,7 +954,7 @@ pub mod unshared_test_utils { pub struct NotifyHandleMock { notify_params: Arc>>, send_message_out: bool, - stop_system_on_count_received_opt: RefCell> + stop_system_on_count_received_opt: RefCell>, } impl Default for NotifyHandleMock { @@ -975,14 +977,15 @@ pub mod unshared_test_utils { self.send_message_out = true; self } - - pub fn stop_system_on_count_received(self, msg_count: usize)-> Self { + + pub fn stop_system_on_count_received(self, msg_count: usize) -> Self { if msg_count == 0 { panic!("Should be a non-zero value") } let system_killer = SystemKillerActor::new(Duration::from_secs(10)); system_killer.start(); - self.stop_system_on_count_received_opt.replace(Some(msg_count)); + self.stop_system_on_count_received_opt + .replace(Some(msg_count)); self } } @@ -994,13 +997,15 @@ pub mod unshared_test_utils { { fn notify<'a>(&'a self, msg: M, ctx: &'a mut Context) { self.notify_params.lock().unwrap().push(msg.clone()); - if let Some(remaining) = self.stop_system_on_count_received_opt.borrow_mut().as_mut() { + if let Some(remaining) = + self.stop_system_on_count_received_opt.borrow_mut().as_mut() + { if remaining == &0 { System::current().stop(); return; - } + } *remaining -= 1; - } + } if self.send_message_out { ctx.notify(msg) } From a12cd50d0db88ac2963391009bdd79972567374c Mon Sep 17 00:00:00 2001 From: Bert Date: Wed, 30 Apr 2025 19:54:13 +0200 Subject: [PATCH 11/49] GH-602: deep dive into schedulers and the awarness of the schedule state --- masq/src/commands/scan_command.rs | 12 +- masq_lib/src/messages.rs | 43 +- .../tests/verify_bill_payment.rs | 6 +- node/src/accountant/mod.rs | 190 ++++++--- node/src/accountant/payment_adjuster.rs | 8 +- node/src/accountant/scanners/mod.rs | 82 +++- .../agent_null.rs | 6 +- .../agent_web3.rs | 6 +- .../blockchain_agent.rs | 0 .../mod.rs | 4 +- .../msgs.rs | 6 +- .../test_utils.rs | 2 +- .../accountant/scanners/scan_schedulers.rs | 375 ++++++++++++++++-- node/src/accountant/test_utils.rs | 4 +- node/src/blockchain/blockchain_bridge.rs | 10 +- .../blockchain_interface_web3/mod.rs | 4 +- .../blockchain_interface_web3/utils.rs | 6 +- .../blockchain/blockchain_interface/mod.rs | 2 +- node/src/node_configurator/configurator.rs | 7 +- node/src/sub_lib/accountant.rs | 2 +- node/src/sub_lib/blockchain_bridge.rs | 4 +- node/src/sub_lib/utils.rs | 4 +- node/src/test_utils/mod.rs | 2 +- node/src/test_utils/recorder.rs | 4 +- .../test_utils/recorder_stop_conditions.rs | 2 +- 25 files changed, 620 insertions(+), 171 deletions(-) rename node/src/accountant/scanners/{payable_scanner => payable_scanner_extension}/agent_null.rs (95%) rename node/src/accountant/scanners/{payable_scanner => payable_scanner_extension}/agent_web3.rs (93%) rename node/src/accountant/scanners/{payable_scanner => payable_scanner_extension}/blockchain_agent.rs (100%) rename node/src/accountant/scanners/{payable_scanner => payable_scanner_extension}/mod.rs (91%) rename node/src/accountant/scanners/{payable_scanner => payable_scanner_extension}/msgs.rs (87%) rename node/src/accountant/scanners/{payable_scanner => payable_scanner_extension}/test_utils.rs (96%) diff --git a/masq/src/commands/scan_command.rs b/masq/src/commands/scan_command.rs index 6faa7be1e..82359d2a5 100644 --- a/masq/src/commands/scan_command.rs +++ b/masq/src/commands/scan_command.rs @@ -3,7 +3,7 @@ use crate::command_context::CommandContext; use crate::commands::commands_common::{transaction, Command, CommandError}; use clap::{App, Arg, SubCommand}; -use masq_lib::messages::{CommendableScanType, UiScanRequest, UiScanResponse}; +use masq_lib::messages::{ScanType, UiScanRequest, UiScanResponse}; use std::fmt::Debug; use std::str::FromStr; @@ -34,7 +34,7 @@ pub fn scan_subcommand() -> App<'static, 'static> { impl Command for ScanCommand { fn execute(&self, context: &mut dyn CommandContext) -> Result<(), CommandError> { let input = UiScanRequest { - scan_type: match CommendableScanType::from_str(&self.name) { + scan_type: match ScanType::from_str(&self.name) { Ok(st) => st, Err(s) => panic!("clap schema does not restrict scan type properly: {}", s), }, @@ -102,12 +102,12 @@ mod tests { #[test] fn scan_command_works() { - scan_command_for_name("payables", CommendableScanType::Payables); - scan_command_for_name("receivables", CommendableScanType::Receivables); - scan_command_for_name("pendingpayables", CommendableScanType::PendingPayables); + scan_command_for_name("payables", ScanType::Payables); + scan_command_for_name("receivables", ScanType::Receivables); + scan_command_for_name("pendingpayables", ScanType::PendingPayables); } - fn scan_command_for_name(name: &str, scan_type: CommendableScanType) { + fn scan_command_for_name(name: &str, scan_type: ScanType) { let transact_params_arc = Arc::new(Mutex::new(vec![])); let mut context = CommandContextMock::new() .transact_params(&transact_params_arc) diff --git a/masq_lib/src/messages.rs b/masq_lib/src/messages.rs index 973b9037b..9f2504e1b 100644 --- a/masq_lib/src/messages.rs +++ b/masq_lib/src/messages.rs @@ -11,6 +11,7 @@ use serde_derive::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt; use std::fmt::Debug; +use std::future::pending; use std::str::FromStr; pub const NODE_UI_PROTOCOL: &str = "MASQNode-UIv2"; @@ -527,6 +528,8 @@ pub struct UiRatePack { pub struct UiScanIntervals { #[serde(rename = "payableSec")] pub payable_sec: u64, + #[serde(rename = "pendingPayableSec")] + pub pending_payable_sec: u64, #[serde(rename = "receivableSec")] pub receivable_sec: u64, } @@ -777,18 +780,20 @@ pub struct UiRecoverWalletsResponse {} conversation_message!(UiRecoverWalletsResponse, "recoverWallets"); #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash)] -pub enum CommendableScanType { +pub enum ScanType { Payables, + PendingPayables, Receivables, } -impl FromStr for CommendableScanType { +impl FromStr for ScanType { type Err = String; fn from_str(s: &str) -> Result { match s { - s if &s.to_lowercase() == "payables" => Ok(CommendableScanType::Payables), - s if &s.to_lowercase() == "receivables" => Ok(CommendableScanType::Receivables), + s if &s.to_lowercase() == "payables" => Ok(ScanType::Payables), + s if &s.to_lowercase() == "pendingpayables" => Ok(ScanType::PendingPayables), + s if &s.to_lowercase() == "receivables" => Ok(ScanType::Receivables), s => Err(format!("Unrecognized ScanType: '{}'", s)), } } @@ -797,7 +802,7 @@ impl FromStr for CommendableScanType { #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct UiScanRequest { #[serde(rename = "scanType")] - pub scan_type: CommendableScanType, + pub scan_type: ScanType, } conversation_message!(UiScanRequest, "scan"); @@ -1153,26 +1158,34 @@ mod tests { #[test] fn scan_type_from_string_happy_path() { - let result: Vec = - vec!["Payables", "pAYABLES", "Receivables", "rECEIVABLES"] - .into_iter() - .map(|s| CommendableScanType::from_str(s).unwrap()) - .collect(); + let result: Vec = vec![ + "Payables", + "pAYABLES", + "PendingPayables", + "pENDINGpAYABLES", + "Receivables", + "rECEIVABLES", + ] + .into_iter() + .map(|s| ScanType::from_str(s).unwrap()) + .collect(); assert_eq!( result, vec![ - CommendableScanType::Payables, - CommendableScanType::Payables, - CommendableScanType::Receivables, - CommendableScanType::Receivables, + ScanType::Payables, + ScanType::Payables, + ScanType::PendingPayables, + ScanType::PendingPayables, + ScanType::Receivables, + ScanType::Receivables, ] ) } #[test] fn scan_type_from_string_error() { - let result = CommendableScanType::from_str("unrecognized"); + let result = ScanType::from_str("unrecognized"); assert_eq!( result, diff --git a/multinode_integration_tests/tests/verify_bill_payment.rs b/multinode_integration_tests/tests/verify_bill_payment.rs index 5d35bf98d..34df5dbca 100644 --- a/multinode_integration_tests/tests/verify_bill_payment.rs +++ b/multinode_integration_tests/tests/verify_bill_payment.rs @@ -3,7 +3,7 @@ use bip39::{Language, Mnemonic, Seed}; use futures::Future; use masq_lib::blockchains::chains::Chain; use masq_lib::constants::WEIS_IN_GWEI; -use masq_lib::messages::{CommendableScanType, ToMessageBody, UiScanRequest}; +use masq_lib::messages::{ScanType, ToMessageBody, UiScanRequest}; use masq_lib::test_utils::utils::UrlHolder; use masq_lib::utils::{derivation_path, find_free_port, NeighborhoodModeLight}; use multinode_integration_tests_lib::blockchain::BlockchainServer; @@ -394,7 +394,7 @@ fn verify_pending_payables() { let ui_client = real_consuming_node.make_ui(ui_port); ui_client.send_request( UiScanRequest { - scan_type: CommendableScanType::Payables, + scan_type: ScanType::Payables, } .tmb(0), ); @@ -432,7 +432,7 @@ fn verify_pending_payables() { ); ui_client.send_request( UiScanRequest { - scan_type: CommendableScanType::Payables, + scan_type: ScanType::Payables, } .tmb(0), ); diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 0ceb67967..c88710758 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -22,10 +22,10 @@ use crate::accountant::db_access_objects::utils::{ use crate::accountant::financials::visibility_restricted_module::{ check_query_is_within_tech_limits, financials_entry_check, }; -use crate::accountant::scanners::payable_scanner::msgs::{ +use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; -use crate::accountant::scanners::{BeginScanError, ScanType, Scanners, UiScanResult}; +use crate::accountant::scanners::{BeginScanError, Scanners, UiScanResult}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, PendingPayableFingerprintSeeds, RetrieveTransactions}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; @@ -57,7 +57,7 @@ use itertools::Either; use itertools::Itertools; use masq_lib::crash_point::CrashPoint; use masq_lib::logger::Logger; -use masq_lib::messages::{CommendableScanType, UiFinancialsResponse}; +use masq_lib::messages::{ScanType, UiFinancialsResponse}; use masq_lib::messages::{FromMessageBody, ToMessageBody, UiFinancialsRequest}; use masq_lib::messages::{ QueryResults, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, @@ -219,9 +219,11 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { - let response_skeleton = msg.response_skeleton_opt; - if self.handle_request_of_scan_for_pending_payable(response_skeleton) { - self.scan_schedulers.payable.schedule_for_new_payable(ctx) + let response_skeleton_opt = msg.response_skeleton_opt; + if self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) { + self.scan_schedulers + .payable + .schedule_for_new_payable(ctx, response_skeleton_opt) } } } @@ -231,7 +233,9 @@ impl Handler for Accountant { fn handle(&mut self, msg: ScanForNewPayables, _ctx: &mut Self::Context) -> Self::Result { let response_skeleton = msg.response_skeleton_opt; - self.handle_request_of_scan_for_new_payable(response_skeleton); + if self.handle_request_of_scan_for_new_payable(response_skeleton) { + todo!() + }; } } @@ -267,7 +271,9 @@ impl Handler for Accountant { .try_send(node_to_ui_msg) .expect("UIGateway is dead") } - self.scan_schedulers.payable.schedule_for_new_payable(ctx) + self.scan_schedulers + .payable + .schedule_for_new_payable(ctx, response_skeleton_opt) } UiScanResult::ChainedScannersUnfinished => self .scan_schedulers @@ -464,11 +470,13 @@ impl Accountant { let payable_dao = dao_factories.payable_dao_factory.make(); let pending_payable_dao = dao_factories.pending_payable_dao_factory.make(); let receivable_dao = dao_factories.receivable_dao_factory.make(); + let (scan_schedulers, scan_schedulers_flags) = ScanSchedulers::new(scan_intervals); let scanners = Scanners::new( dao_factories, Rc::new(payment_thresholds), config.when_pending_too_long_sec, Rc::clone(&financial_statistics), + scan_schedulers_flags, ); Accountant { @@ -480,7 +488,7 @@ impl Accountant { pending_payable_dao, scanners, crashable: config.crash_point == CrashPoint::Message, - scan_schedulers: ScanSchedulers::new(scan_intervals), + scan_schedulers, financial_statistics: Rc::clone(&financial_statistics), outbound_payments_instructions_sub_opt: None, qualified_payables_sub_opt: None, @@ -869,7 +877,7 @@ impl Accountant { fn handle_request_of_scan_for_new_payable( &mut self, response_skeleton_opt: Option, - ) { + ) -> bool { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( @@ -888,12 +896,22 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); + false + } + Err(e) => { + e.handle_error( + &self.logger, + ScanType::Payables, + response_skeleton_opt.is_some(), + ); + + //TODO simplify after having been properly tested + if e == BeginScanError::NothingToProcess { + todo!() + } else { + false + } } - Err(e) => e.handle_error( - &self.logger, - ScanType::Payables, - response_skeleton_opt.is_some(), - ), } } @@ -996,17 +1014,21 @@ impl Accountant { fn handle_externally_triggered_scan( &mut self, _ctx: &mut Context, - scan_type: CommendableScanType, + scan_type: ScanType, response_skeleton: ResponseSkeleton, ) { match scan_type { - CommendableScanType::Payables => { - todo!(); + ScanType::Payables => { + if self.handle_request_of_scan_for_new_payable(Some(response_skeleton)) { + todo!() + } + } + ScanType::PendingPayables => { if self.handle_request_of_scan_for_pending_payable(Some(response_skeleton)) { todo!() } } - CommendableScanType::Receivables => { + ScanType::Receivables => { self.handle_request_of_scan_for_receivable(Some(response_skeleton)) } } @@ -1118,7 +1140,7 @@ mod tests { use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; use crate::accountant::payment_adjuster::Adjustment; - use crate::accountant::scanners::payable_scanner::test_utils::BlockchainAgentMock; + use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; use crate::accountant::scanners::test_utils::{NewPayableScanDynIntervalComputerMock}; use crate::accountant::scanners::{BeginScanError}; use crate::accountant::test_utils::DaoWithDestination::{ @@ -1384,7 +1406,7 @@ mod tests { } #[test] - fn scan_receivables_request() { + fn externally_triggered_scan_receivables_request() { let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), @@ -1408,7 +1430,7 @@ mod tests { let ui_message = NodeFromUiMessage { client_id: 1234, body: UiScanRequest { - scan_type: CommendableScanType::Receivables, + scan_type: ScanType::Receivables, } .tmb(4321), }; @@ -1477,35 +1499,35 @@ mod tests { } #[test] - fn external_scan_payables_request() { - let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - config.suppress_initial_scans = true; - let fingerprint = PendingPayableFingerprint { - rowid: 1234, - timestamp: SystemTime::now(), - hash: Default::default(), - attempt: 1, - amount: 1_000_000, - process_error: None, + fn externally_triggered_scan_payables_request() { + let config = bc_from_earning_wallet(make_wallet("some_wallet_address")); + let consuming_wallet = make_paying_wallet(b"consuming"); + let payable_account = PayableAccount { + wallet: make_wallet("wallet"), + balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), + last_paid_timestamp: SystemTime::now().sub(Duration::from_secs( + (DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 1) as u64, + )), + pending_payable_opt: None, }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![fingerprint.clone()]); + let payable_dao = + PayableDaoMock::new().non_pending_payables_result(vec![payable_account.clone()]); let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) .bootstrapper_config(config) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .payable_daos(vec![ForPayableScanner(payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge = blockchain_bridge - .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)); + .system_stop_conditions(match_lazily_every_type_id!(QualifiedPayablesMessage)); let blockchain_bridge_addr = blockchain_bridge.start(); - subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); let subject_addr = subject.start(); let system = System::new("test"); let ui_message = NodeFromUiMessage { client_id: 1234, body: UiScanRequest { - scan_type: CommendableScanType::Payables, + scan_type: ScanType::Payables, } .tmb(4321), }; @@ -1515,13 +1537,14 @@ mod tests { system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); assert_eq!( - blockchain_bridge_recording.get_record::(0), - &RequestTransactionReceipts { - pending_payable: vec![fingerprint], + blockchain_bridge_recording.get_record::(0), + &QualifiedPayablesMessage { + qualified_payables: vec![payable_account], + consuming_wallet, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321, - }), + }) } ); } @@ -1556,7 +1579,7 @@ mod tests { let ui_message = NodeFromUiMessage { client_id: 1234, body: UiScanRequest { - scan_type: CommendableScanType::Payables, + scan_type: ScanType::Payables, } .tmb(4321), }; @@ -1827,9 +1850,66 @@ mod tests { } #[test] - fn scan_request_from_ui_is_not_handled_in_case_the_scan_is_already_running() { + fn externally_triggered_scan_pending_payables_request() { + let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); + config.suppress_initial_scans = true; + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + receivable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_secs(100), + }); + let fingerprint = PendingPayableFingerprint { + rowid: 1234, + timestamp: SystemTime::now(), + hash: Default::default(), + attempt: 1, + amount: 1_000_000, + process_error: None, + }; + let pending_payable_dao = PendingPayableDaoMock::default() + .return_all_errorless_fingerprints_result(vec![fingerprint.clone()]); + let subject = AccountantBuilder::default() + .consuming_wallet(make_paying_wallet(b"consuming")) + .bootstrapper_config(config) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let subject_addr = subject.start(); + let system = System::new("test"); + let peer_actors = peer_actors_builder() + .blockchain_bridge(blockchain_bridge) + .build(); + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + let ui_message = NodeFromUiMessage { + client_id: 1234, + body: UiScanRequest { + scan_type: ScanType::PendingPayables, + } + .tmb(4321), + }; + + subject_addr.try_send(ui_message).unwrap(); + + System::current().stop(); + system.run(); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + assert_eq!( + blockchain_bridge_recording.get_record::(0), + &RequestTransactionReceipts { + pending_payable: vec![fingerprint], + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 1234, + context_id: 4321, + }), + } + ); + } + + #[test] + fn externally_triggered_scan_is_not_handled_in_case_the_scan_is_already_running() { init_test_logging(); - let test_name = "scan_request_from_ui_is_not_handled_in_case_the_scan_is_already_running"; + let test_name = + "externally_triggered_scan_is_not_handled_in_case_the_scan_is_already_running"; let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); config.suppress_initial_scans = true; let fingerprint = PendingPayableFingerprint { @@ -1857,7 +1937,7 @@ mod tests { let first_message = NodeFromUiMessage { client_id: 1234, body: UiScanRequest { - scan_type: CommendableScanType::Payables, + scan_type: ScanType::Payables, } .tmb(4321), }; @@ -3668,8 +3748,10 @@ mod tests { subject .scan_schedulers .payable - .last_new_payable_scan_timestamp - .replace(last_new_payable_scan_timestamp); + .inner + .lock() + .unwrap() + .last_new_payable_scan_timestamp = last_new_payable_scan_timestamp; subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); @@ -3742,8 +3824,10 @@ mod tests { subject .scan_schedulers .payable - .last_new_payable_scan_timestamp - .replace(last_new_payable_scan_timestamp); + .inner + .lock() + .unwrap() + .last_new_payable_scan_timestamp = last_new_payable_scan_timestamp; subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); @@ -3806,8 +3890,10 @@ mod tests { subject .scan_schedulers .payable - .last_new_payable_scan_timestamp - .replace(last_new_payable_scan_timestamp); + .inner + .lock() + .unwrap() + .last_new_payable_scan_timestamp = last_new_payable_scan_timestamp; subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); diff --git a/node/src/accountant/payment_adjuster.rs b/node/src/accountant/payment_adjuster.rs index ba4cd40d1..74c88690b 100644 --- a/node/src/accountant/payment_adjuster.rs +++ b/node/src/accountant/payment_adjuster.rs @@ -1,7 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::payable_scanner::PreparedAdjustment; +use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use masq_lib::logger::Logger; use std::time::SystemTime; @@ -71,8 +71,8 @@ pub enum AnalysisError {} #[cfg(test)] mod tests { use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; - use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; - use crate::accountant::scanners::payable_scanner::test_utils::BlockchainAgentMock; + use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; + use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; use crate::accountant::test_utils::make_payable_account; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 9b971f93e..0711f9228 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -pub mod payable_scanner; +pub mod payable_scanner_extension; pub mod scan_schedulers; pub mod scanners_utils; pub mod test_utils; @@ -37,30 +37,25 @@ use actix::{Message}; use itertools::{Either, Itertools}; use masq_lib::logger::Logger; use masq_lib::logger::TIME_FORMATTING_STRING; -use masq_lib::messages::{ToMessageBody, UiScanResponse}; +use masq_lib::messages::{ScanType, ToMessageBody, UiScanResponse}; use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; use std::rc::Rc; +use std::sync::{Arc, RwLock}; use std::time::{SystemTime}; use time::format_description::parse; use time::OffsetDateTime; use web3::types::H256; -use crate::accountant::scanners::payable_scanner::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; -use crate::accountant::scanners::payable_scanner::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; +use crate::accountant::scanners::payable_scanner_extension::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; +use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; +use crate::accountant::scanners::scan_schedulers::{ScanSchedulers, ScanSchedulersFlags}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::db_config::persistent_configuration::{PersistentConfiguration, PersistentConfigurationReal}; -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] -pub enum ScanType { - PendingPayables, - Payables, - Receivables, -} - pub struct Scanners { pub payable: Box>, pub pending_payable: Box< @@ -73,6 +68,7 @@ pub struct Scanners { pub receivable: Box< dyn StartableAccessibleScanner, >, + scan_schedulers_flags: Rc>, } impl Scanners { @@ -81,6 +77,7 @@ impl Scanners { payment_thresholds: Rc, when_pending_too_long_sec: u64, financial_statistics: Rc>, + scan_schedulers_flags: Rc>, ) -> Self { let payable = Box::new(PayableScanner::new( dao_factories.payable_dao_factory.make(), @@ -112,6 +109,7 @@ impl Scanners { payable, pending_payable, receivable, + scan_schedulers_flags, } } @@ -122,10 +120,22 @@ impl Scanners { response_skeleton_opt: Option, logger: &Logger, ) -> Result { + let triggered_manually = response_skeleton_opt.is_some(); + if triggered_manually { + if !self + .scan_schedulers_flags + .borrow() + .pending_payable_sequence_is_ongoing + { + todo!() // ManualTriggerError + } + } + match ( self.pending_payable.scan_started_at(), self.payable.scan_started_at(), ) { + // TODO this might be possible!!! (Some(pp_timestamp), Some(p_timestamp)) => unreachable!( "Both payable scanners should never be allowed to run in parallel. Scan for \ pending payables started at: {}, scan for payables started at: {}", @@ -161,6 +171,18 @@ impl Scanners { response_skeleton_opt: Option, logger: &Logger, ) -> Result { + // Under normal circumstances, it is guaranteed that the new-payable scanner will never + // overlap with the execution of the pending payable scanner, as they are safely + // and automatically scheduled to run sequentially. However, since we allow for unexpected, + // manually triggered scans, a conflict is possible and must be handled. + if self + .scan_schedulers_flags + .borrow() + .pending_payable_sequence_is_ongoing + { + todo!() // ManualTriggerError + } + if let Some(started_at) = self.payable.scan_started_at() { return Err(BeginScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Payables, @@ -184,6 +206,7 @@ impl Scanners { logger: &Logger, ) -> Result { if let Some(started_at) = self.payable.scan_started_at() { + //TODO Oh god, this won't work with the externally triggered scanners!!! unreachable!( "Guard for pending payables should've prevented running the tandem of scanners \ if the payable scanner was still running. It started {} and is still running at {}", @@ -822,7 +845,10 @@ impl AccessibleScanner match message.fingerprints_with_receipts.is_empty() { true => { debug!(logger, "No transaction receipts found."); - todo!("requires payments retry"); + todo!( + "requires payment retry...it must be processed across the new methods on \ + the SentPaybleDAO" + ); } false => { debug!( @@ -1213,6 +1239,7 @@ pub enum BeginScanError { started_at: SystemTime, }, CalledFromNullScanner, // Exclusive for tests + ManualTriggerError(String), } impl BeginScanError { @@ -1242,6 +1269,7 @@ impl BeginScanError { true => None, false => panic!("Null Scanner shouldn't be running inside production code."), }, + BeginScanError::ManualTriggerError(msg) => todo!(), }; if let Some(log_message) = log_message_opt { @@ -1278,7 +1306,7 @@ impl BeginScanError { // from this file #[cfg(test)] pub mod local_test_utils { - use crate::accountant::scanners::payable_scanner::msgs::QualifiedPayablesMessage; + use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::{ AccessibleScanner, BeginScanError, InaccessibleScanner, MultistageDualPayableScanner, PreparedAdjustment, PrivateScanStarter, ScanWithStarter, @@ -1588,10 +1616,10 @@ mod tests { PendingPayable, PendingPayableDaoError, TransactionHashes, }; use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t}; - use crate::accountant::scanners::payable_scanner::msgs::QualifiedPayablesMessage; + use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PendingPayableMetadata; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; - use crate::accountant::scanners::{AccessibleScanner, BeginScanError, InaccessibleScanner, PayableScanner, PendingPayableScanner, PrivateScanStarter, ReceivableScanner, ScanType, ScanWithStarter, ScannerCommon, Scanners}; + use crate::accountant::scanners::{AccessibleScanner, BeginScanError, InaccessibleScanner, PayableScanner, PendingPayableScanner, PrivateScanStarter, ReceivableScanner, ScanWithStarter, ScannerCommon, Scanners}; use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanForNewPayables, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; @@ -1623,11 +1651,13 @@ mod tests { use std::ops::Sub; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::rc::Rc; - use std::sync::{Arc, Mutex}; + use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration, SystemTime}; use web3::types::{TransactionReceipt, H256}; use web3::Error; + use masq_lib::messages::ScanType; use crate::accountant::scanners::local_test_utils::NullScanner; + use crate::accountant::scanners::scan_schedulers::ScanSchedulersFlags; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; #[test] @@ -1654,6 +1684,7 @@ mod tests { let payment_thresholds = make_custom_payment_thresholds(); let payment_thresholds_rc = Rc::new(payment_thresholds); let initial_rc_count = Rc::strong_count(&payment_thresholds_rc); + let scan_schedulers_flags = Rc::new(RefCell::new(ScanSchedulersFlags::default())); let mut scanners = Scanners::new( DaoFactories { @@ -1666,6 +1697,7 @@ mod tests { Rc::clone(&payment_thresholds_rc), when_pending_too_long_sec, Rc::new(RefCell::new(financial_statistics.clone())), + Rc::clone(&scan_schedulers_flags), ); let payable_scanner = scanners @@ -1726,6 +1758,23 @@ mod tests { Rc::strong_count(&payment_thresholds_rc), initial_rc_count + 3 ); + assert_eq!( + scanners + .scan_schedulers_flags + .borrow() + .pending_payable_sequence_is_ongoing, + false + ); + scan_schedulers_flags + .borrow_mut() + .pending_payable_sequence_is_ongoing = true; + assert_eq!( + scanners + .scan_schedulers_flags + .borrow() + .pending_payable_sequence_is_ongoing, + true + ); } #[test] @@ -4181,6 +4230,7 @@ mod tests { payable: Box::new(NullScanner::new()), pending_payable: Box::new(NullScanner::new()), receivable: Box::new(NullScanner::new()), + scan_schedulers_flags: Rc::new(RefCell::new(ScanSchedulersFlags::default())), } } diff --git a/node/src/accountant/scanners/payable_scanner/agent_null.rs b/node/src/accountant/scanners/payable_scanner_extension/agent_null.rs similarity index 95% rename from node/src/accountant/scanners/payable_scanner/agent_null.rs rename to node/src/accountant/scanners/payable_scanner_extension/agent_null.rs index 9c70c3dc3..5f9811204 100644 --- a/node/src/accountant/scanners/payable_scanner/agent_null.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/agent_null.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; @@ -77,8 +77,8 @@ impl Default for BlockchainAgentNull { #[cfg(test)] mod tests { - use crate::accountant::scanners::payable_scanner::agent_null::BlockchainAgentNull; - use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; + use crate::accountant::scanners::payable_scanner_extension::agent_null::BlockchainAgentNull; + use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; diff --git a/node/src/accountant/scanners/payable_scanner/agent_web3.rs b/node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs similarity index 93% rename from node/src/accountant/scanners/payable_scanner/agent_web3.rs rename to node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs index de1a87aa5..8acf40ef8 100644 --- a/node/src/accountant/scanners/payable_scanner/agent_web3.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use masq_lib::blockchains::chains::Chain; @@ -64,10 +64,10 @@ impl BlockchainAgentWeb3 { #[cfg(test)] mod tests { - use crate::accountant::scanners::payable_scanner::agent_web3::{ + use crate::accountant::scanners::payable_scanner_extension::agent_web3::{ BlockchainAgentWeb3, WEB3_MAXIMAL_GAS_LIMIT_MARGIN, }; - use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; + use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::test_utils::make_wallet; use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; diff --git a/node/src/accountant/scanners/payable_scanner/blockchain_agent.rs b/node/src/accountant/scanners/payable_scanner_extension/blockchain_agent.rs similarity index 100% rename from node/src/accountant/scanners/payable_scanner/blockchain_agent.rs rename to node/src/accountant/scanners/payable_scanner_extension/blockchain_agent.rs diff --git a/node/src/accountant/scanners/payable_scanner/mod.rs b/node/src/accountant/scanners/payable_scanner_extension/mod.rs similarity index 91% rename from node/src/accountant/scanners/payable_scanner/mod.rs rename to node/src/accountant/scanners/payable_scanner_extension/mod.rs index bc4f525c6..54a738f16 100644 --- a/node/src/accountant/scanners/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/mod.rs @@ -7,7 +7,7 @@ pub mod msgs; pub mod test_utils; use crate::accountant::payment_adjuster::Adjustment; -use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; use crate::accountant::scanners::{AccessibleScanner, ScanWithStarter}; use crate::accountant::{ScanForNewPayables, ScanForRetryPayables}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; @@ -59,7 +59,7 @@ impl PreparedAdjustment { #[cfg(test)] mod tests { - use crate::accountant::scanners::payable_scanner::PreparedAdjustment; + use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; impl Clone for PreparedAdjustment { fn clone(&self) -> Self { diff --git a/node/src/accountant/scanners/payable_scanner/msgs.rs b/node/src/accountant/scanners/payable_scanner_extension/msgs.rs similarity index 87% rename from node/src/accountant/scanners/payable_scanner/msgs.rs rename to node/src/accountant/scanners/payable_scanner_extension/msgs.rs index 49bb93909..599f17390 100644 --- a/node/src/accountant/scanners/payable_scanner/msgs.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/msgs.rs @@ -1,7 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::accountant::{ResponseSkeleton, SkeletonOptHolder}; use crate::sub_lib::wallet::Wallet; use actix::Message; @@ -58,8 +58,8 @@ impl BlockchainAgentWithContextMessage { #[cfg(test)] mod tests { - use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; - use crate::accountant::scanners::payable_scanner::test_utils::BlockchainAgentMock; + use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; + use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; impl Clone for BlockchainAgentWithContextMessage { fn clone(&self) -> Self { diff --git a/node/src/accountant/scanners/payable_scanner/test_utils.rs b/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs similarity index 96% rename from node/src/accountant/scanners/payable_scanner/test_utils.rs rename to node/src/accountant/scanners/payable_scanner_extension/test_utils.rs index 921266bbd..def16f20e 100644 --- a/node/src/accountant/scanners/payable_scanner/test_utils.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs @@ -2,7 +2,7 @@ #![cfg(test)] -use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 149ec5dfe..c4f7f6547 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -8,51 +8,83 @@ use crate::sub_lib::accountant::ScanIntervals; use crate::sub_lib::utils::{ NotifyHandle, NotifyHandleReal, NotifyLaterHandle, NotifyLaterHandleReal, }; -use actix::{Context, Handler}; +use actix::{Actor, Context, Handler}; use std::cell::RefCell; +use std::marker::PhantomData; +use std::rc::Rc; +use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; pub struct ScanSchedulers { - pub payable: PayableScanScheduler, - pub pending_payable: SimplePeriodicalScanScheduler, - pub receivable: SimplePeriodicalScanScheduler, + pub payable: PayableScanScheduler, + pub pending_payable: SimplePeriodicalScanScheduler, + pub receivable: SimplePeriodicalScanScheduler, + pub scan_schedulers_flags: Rc>, } impl ScanSchedulers { - pub fn new(scan_intervals: ScanIntervals) -> Self { - Self { - payable: PayableScanScheduler::new(scan_intervals.payable_scan_interval), - pending_payable: SimplePeriodicalScanScheduler::new( - scan_intervals.pending_payable_scan_interval, - ), - receivable: SimplePeriodicalScanScheduler::new(scan_intervals.receivable_scan_interval), - } + pub fn new(scan_intervals: ScanIntervals) -> (Self, Rc>) { + let scan_schedulers_flags = Rc::new(RefCell::new(ScanSchedulersFlags::default())); + + ( + Self { + payable: PayableScanScheduler::new(scan_intervals.payable_scan_interval), + pending_payable: SimplePeriodicalScanScheduler::new( + scan_intervals.pending_payable_scan_interval, + ), + receivable: SimplePeriodicalScanScheduler::new( + scan_intervals.receivable_scan_interval, + ), + scan_schedulers_flags: Rc::clone(&scan_schedulers_flags), + }, + scan_schedulers_flags, + ) } } -pub struct PayableScanScheduler { - pub new_payable_notify_later: Box>, +#[derive(Default)] +pub struct ScanSchedulersFlags { + pub pending_payable_sequence_is_ongoing: bool, +} + +#[derive(Debug, PartialEq)] +pub enum PayableScanSchedulerError { + ScanForNewPayableAlreadyScheduled, +} + +pub struct PayableScanScheduler { + pub new_payable_notify_later: Box>, pub dyn_interval_computer: Box, - pub last_new_payable_scan_timestamp: RefCell, + pub inner: Arc>, pub nominal_interval: Duration, - pub new_payable_notify: Box>, - pub retry_payable_notify: Box>, + pub new_payable_notify: Box>, + pub retry_payable_notify: Box>, } -impl PayableScanScheduler { +impl PayableScanScheduler +where + ActorType: Actor> + + Handler + + Handler, +{ fn new(nominal_interval: Duration) -> Self { Self { new_payable_notify_later: Box::new(NotifyLaterHandleReal::default()), dyn_interval_computer: Box::new(NewPayableScanDynIntervalComputerReal::default()), - last_new_payable_scan_timestamp: RefCell::new(UNIX_EPOCH), + inner: Arc::new(Mutex::new(PayableScanSchedulerInner::default())), nominal_interval, new_payable_notify: Box::new(NotifyHandleReal::default()), retry_payable_notify: Box::new(NotifyHandleReal::default()), } } - pub fn schedule_for_new_payable(&self, ctx: &mut Context) { - let last_new_payable_timestamp = *self.last_new_payable_scan_timestamp.borrow(); + pub fn schedule_for_new_payable( + &self, + ctx: &mut Context, + response_skeleton_opt: Option, + ) { + let inner = self.inner.lock().expect("couldn't acquire inner"); + let last_new_payable_timestamp = inner.last_new_payable_scan_timestamp; let nominal_interval = self.nominal_interval; let now = SystemTime::now(); if let Some(interval) = self.dyn_interval_computer.compute_interval( @@ -78,11 +110,11 @@ impl PayableScanScheduler { } // Can be triggered by a command, whereas the finished pending payable scanner is followed up - // by this scheduled message. It is inserted into the Accountant's mailbox right away (no - // interval) + // by this scheduled message that can bear the response skeleton. This message is inserted into + // the Accountant's mailbox with no delay pub fn schedule_for_retry_payable( &self, - ctx: &mut Context, + ctx: &mut Context, response_skeleton_opt: Option, ) { self.retry_payable_notify.notify( @@ -94,6 +126,46 @@ impl PayableScanScheduler { } } +pub struct PayableScanSchedulerInner { + pub last_new_payable_scan_timestamp: SystemTime, + pub next_new_payable_scan_already_scheduled: bool, +} + +impl Default for PayableScanSchedulerInner { + fn default() -> Self { + Self { + last_new_payable_scan_timestamp: UNIX_EPOCH, + next_new_payable_scan_already_scheduled: false, + } + } +} + +pub trait AutomaticSchedulingAwareScanner { + fn is_currently_automatically_scheduled(&self, spec: ScannerSpecification) -> bool; + fn mark_as_automatically_scheduled(&self, spec: ScannerSpecification); + fn mark_as_already_automatically_utilized(&self, spec: ScannerSpecification); +} + +impl AutomaticSchedulingAwareScanner for PayableScanScheduler { + fn is_currently_automatically_scheduled(&self, spec: PayableScannerMode) -> bool { + todo!() + } + + fn mark_as_automatically_scheduled(&self, spec: PayableScannerMode) { + todo!() + } + + fn mark_as_already_automatically_utilized(&self, spec: PayableScannerMode) { + todo!() + } +} + +#[derive(Clone, Copy)] +pub enum PayableScannerMode { + Retry, + NewPayable, +} + pub trait NewPayableScanDynIntervalComputer { fn compute_interval( &self, @@ -129,41 +201,73 @@ impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerReal } } -pub struct SimplePeriodicalScanScheduler { - pub handle: Box>, +pub struct SimplePeriodicalScanScheduler { + phantom: PhantomData, + pub is_currently_automatically_scheduled: RefCell, + pub handle: Box>, pub interval: Duration, } -impl SimplePeriodicalScanScheduler +impl SimplePeriodicalScanScheduler where - Message: actix::Message + Default + 'static, - Accountant: Handler, + Message: actix::Message + Default + 'static + Send, + ActorType: Actor> + Handler, { fn new(interval: Duration) -> Self { Self { + phantom: PhantomData::default(), + is_currently_automatically_scheduled: RefCell::new(false), handle: Box::new(NotifyLaterHandleReal::default()), interval, } } pub fn schedule( &self, - ctx: &mut Context, - _response_skeleton_opt: Option, + ctx: &mut Context, + response_skeleton_opt: Option, ) { - // the default of the message implies response_skeleton_opt to be None - // because scheduled scans don't respond + // The default of the message implies response_skeleton_opt to be None because scheduled + // scans don't respond let _ = self .handle .notify_later(Message::default(), self.interval, ctx); + + if response_skeleton_opt == None { + self.mark_as_automatically_scheduled(()) + } + } +} + +impl AutomaticSchedulingAwareScanner<()> for SimplePeriodicalScanScheduler { + fn is_currently_automatically_scheduled(&self, _spec: ()) -> bool { + *self.is_currently_automatically_scheduled.borrow() + } + + fn mark_as_automatically_scheduled(&self, _spec: ()) { + self.is_currently_automatically_scheduled.replace(true); + } + + fn mark_as_already_automatically_utilized(&self, _spec: ()) { + self.is_currently_automatically_scheduled.replace(false); } } #[cfg(test)] mod tests { use crate::accountant::scanners::scan_schedulers::{ - NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, ScanSchedulers, + AutomaticSchedulingAwareScanner, NewPayableScanDynIntervalComputer, + NewPayableScanDynIntervalComputerReal, PayableScanScheduler, PayableScanSchedulerError, + PayableScanSchedulerInner, PayableScannerMode, ScanSchedulers, + SimplePeriodicalScanScheduler, + }; + use crate::accountant::{ + ResponseSkeleton, ScanForNewPayables, ScanForPendingPayables, ScanForReceivables, + ScanForRetryPayables, SkeletonOptHolder, }; use crate::sub_lib::accountant::ScanIntervals; + use crate::test_utils::unshared_test_utils::system_killer_actor::SystemKillerActor; + use actix::{Actor, Context, Handler, Message, System}; + use std::marker::PhantomData; use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[test] @@ -174,24 +278,66 @@ mod tests { receivable_scan_interval: Duration::from_secs(7), }; - let result = ScanSchedulers::new(scan_intervals); + let (schedulers, schedulers_flags) = ScanSchedulers::new(scan_intervals); assert_eq!( - result.payable.nominal_interval, + schedulers.payable.nominal_interval, scan_intervals.payable_scan_interval ); + let payable_scheduler_inner = schedulers.payable.inner.lock().unwrap(); assert_eq!( - *result.payable.last_new_payable_scan_timestamp.borrow(), + payable_scheduler_inner.last_new_payable_scan_timestamp, UNIX_EPOCH ); assert_eq!( - result.pending_payable.interval, + payable_scheduler_inner.next_new_payable_scan_already_scheduled, + false + ); + assert_eq!( + schedulers.pending_payable.interval, scan_intervals.pending_payable_scan_interval ); assert_eq!( - result.receivable.interval, + *schedulers + .pending_payable + .is_currently_automatically_scheduled + .borrow(), + false + ); + assert_eq!( + schedulers.receivable.interval, scan_intervals.receivable_scan_interval - ) + ); + assert_eq!( + *schedulers + .receivable + .is_currently_automatically_scheduled + .borrow(), + false + ); + assert_eq!( + schedulers + .scan_schedulers_flags + .borrow() + .pending_payable_sequence_is_ongoing, + false + ); + assert_eq!( + schedulers_flags + .borrow() + .pending_payable_sequence_is_ongoing, + false + ); + schedulers + .scan_schedulers_flags + .borrow_mut() + .pending_payable_sequence_is_ongoing = true; + assert_eq!( + schedulers_flags + .borrow() + .pending_payable_sequence_is_ongoing, + true + ); } #[test] @@ -300,4 +446,153 @@ mod tests { Duration::from_secs(32), ); } + + #[derive(Message)] + struct TestSetupCarrierMsg { + scheduler: Scheduler, + automated_message: MessageToSchedule, + user_triggered_message: MessageToSchedule, + schedule_scanner: ScheduleScanner, + scan_specs: ScannerSpecs, + } + + struct ActixContextProvidingActor {} + + impl Actor for ActixContextProvidingActor { + type Context = Context; + } + + impl Handler for ActixContextProvidingActor { + type Result = (); + + fn handle(&mut self, _msg: ScanForNewPayables, _ctx: &mut Self::Context) -> Self::Result { + intentionally_blank!() + } + } + + impl Handler for ActixContextProvidingActor { + type Result = (); + + fn handle(&mut self, _msg: ScanForRetryPayables, _ctx: &mut Self::Context) -> Self::Result { + intentionally_blank!() + } + } + + impl Handler for ActixContextProvidingActor { + type Result = (); + + fn handle(&mut self, _msg: ScanForReceivables, _ctx: &mut Self::Context) -> Self::Result { + intentionally_blank!() + } + } + + impl + Handler> + for ActixContextProvidingActor + where + ScheduleScanner: Fn(&Scheduler, MessageToSchedule, &mut Self::Context), + Scheduler: AutomaticSchedulingAwareScanner, + ScannerSpecs: Copy, + { + type Result = (); + + fn handle( + &mut self, + msg: TestSetupCarrierMsg, + ctx: &mut Self::Context, + ) -> Self::Result { + let scheduler = msg.scheduler; + let scan_specs = msg.scan_specs; + + let first_state = scheduler.is_currently_automatically_scheduled(scan_specs); + scheduler.mark_as_automatically_scheduled(scan_specs); + let second_state = scheduler.is_currently_automatically_scheduled(scan_specs); + scheduler.mark_as_already_automatically_utilized(scan_specs); + let third_state = scheduler.is_currently_automatically_scheduled(scan_specs); + (&msg.schedule_scanner)(&scheduler, msg.automated_message, ctx); + let fourth_state = scheduler.is_currently_automatically_scheduled(scan_specs); + scheduler.mark_as_already_automatically_utilized(scan_specs); + let fifth_state = scheduler.is_currently_automatically_scheduled(scan_specs); + (&msg.schedule_scanner)(&scheduler, msg.user_triggered_message, ctx); + let sixth_state = scheduler.is_currently_automatically_scheduled(scan_specs); + + assert_eq!(first_state, false); + assert_eq!(second_state, true); + assert_eq!(third_state, false); + assert_eq!(fourth_state, true); + assert_eq!(fifth_state, false); + assert_eq!(sixth_state, false); + System::current().stop(); + } + } + + #[test] + fn scheduling_registration_on_simple_periodical_scanner_works() { + let system = System::new("test"); + let system_killer = SystemKillerActor::new(Duration::from_secs(10)); + system_killer.start(); + let test_performer_addr = ActixContextProvidingActor {}.start(); + let duration = Duration::from_secs(1000); + let scheduler = SimplePeriodicalScanScheduler::new(duration); + let automated_message = ScanForReceivables { + response_skeleton_opt: None, + }; + let user_triggered_message = ScanForReceivables { + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 12, + context_id: 7, + }), + }; + let schedule_scanner = + |scheduler: &SimplePeriodicalScanScheduler< + ScanForReceivables, + ActixContextProvidingActor, + >, + msg: ScanForReceivables, + ctx: &mut Context| { + scheduler.schedule(ctx, msg.response_skeleton_opt); + }; + let scan_specs = (); + let msg = TestSetupCarrierMsg { + scheduler, + automated_message, + user_triggered_message, + schedule_scanner, + scan_specs, + }; + + test_performer_addr.try_send(msg).unwrap(); + + assert_eq!(system.run(), 0) + } + + #[test] + fn scheduling_registration_for_new_payable_on_notify_later_handle_works_in_complex_scheduler() { + todo!( + "maybe now first consider where it's gonna help you to know the current schedule state" + ) + // let system = System::new("test"); + // let system_killer = SystemKillerActor::new(Duration::from_secs(10)); + // system_killer.start(); + // let test_performer_addr = ActixContextProvidingActor {}.start(); + // let duration = Duration::from_secs(1000); + // let scheduler = PayableScanScheduler::new(duration); + // let automated_message = ScanForNewPayables{ response_skeleton_opt: None }; + // let user_triggered_message = ScanForNewPayables{response_skeleton_opt: Some(ResponseSkeleton{ client_id: 12, context_id: 7 })}; + // let schedule_scanner = |scheduler: &PayableScanScheduler, msg: ScanForReceivables, ctx: &mut Context|{ + // scheduler.schedule_for_new_payable(ctx, msg.response_skeleton_opt); + // }; + // let scan_specs = PayableScannerMode::NewPayable; + // let msg = TestSetupCarrierMsg { + // scheduler, + // automated_message, + // user_triggered_message, + // schedule_scanner, + // scan_specs, + // }; + // + // test_performer_addr.try_send(msg).unwrap(); + // + // assert_eq!(system.run(), 0) + } } diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 160aff952..a0bb71fa7 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -14,8 +14,8 @@ use crate::accountant::db_access_objects::receivable_dao::{ }; use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdjuster}; -use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::payable_scanner::PreparedAdjustment; +use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; use crate::accountant::scanners::{PayableScanner, PendingPayableScanner, ReceivableScanner}; use crate::accountant::{gwei_to_wei, Accountant, DEFAULT_PENDING_TOO_LONG_SEC}; diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 1f82b759c..2aa576dc9 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::payable_scanner::msgs::{ +use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; use crate::accountant::{ @@ -44,9 +44,9 @@ use std::sync::{Arc, Mutex}; use std::time::SystemTime; use ethabi::Hash; use web3::types::H256; +use masq_lib::messages::ScanType; use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; -use crate::accountant::scanners::ScanType; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; pub const CRASH_KEY: &str = "BLOCKCHAINBRIDGE"; @@ -549,8 +549,8 @@ mod tests { use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; use crate::accountant::db_access_objects::utils::from_time_t; - use crate::accountant::scanners::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; - use crate::accountant::scanners::payable_scanner::test_utils::BlockchainAgentMock; + use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; + use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; use crate::accountant::test_utils::{make_payable_account, make_pending_payable_fingerprint}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::BlockchainInterfaceWeb3; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError::TransactionID; diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs index a0269c3ab..852b02a4e 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -4,7 +4,7 @@ pub mod lower_level_interface_web3; mod utils; use std::cmp::PartialEq; -use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; @@ -430,7 +430,7 @@ impl BlockchainInterfaceWeb3 { #[cfg(test)] mod tests { use super::*; - use crate::accountant::scanners::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; + use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_LITERAL, TRANSFER_METHOD_ID, diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs index f8cc16d71..75806e1d4 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -2,8 +2,8 @@ use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; -use crate::accountant::scanners::payable_scanner::agent_web3::BlockchainAgentWeb3; -use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::agent_web3::BlockchainAgentWeb3; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, HashAndAmount, TRANSFER_METHOD_ID, @@ -332,7 +332,7 @@ mod tests { use super::*; use crate::accountant::db_access_objects::utils::from_time_t; use crate::accountant::gwei_to_wei; - use crate::accountant::scanners::payable_scanner::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; + use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::accountant::test_utils::{ make_payable_account, make_payable_account_with_wallet_and_balance_and_timestamp_opt, }; diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index 7bd5e5ca8..bdcbf6a91 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -6,7 +6,7 @@ pub mod lower_level_interface; use actix::Recipient; use ethereum_types::H256; -use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainAgentBuildError, BlockchainError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RetrievedBlockchainTransactions}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; diff --git a/node/src/node_configurator/configurator.rs b/node/src/node_configurator/configurator.rs index fdf0199cd..b025abef9 100644 --- a/node/src/node_configurator/configurator.rs +++ b/node/src/node_configurator/configurator.rs @@ -614,6 +614,7 @@ impl Configurator { let routing_service_rate = rate_pack.routing_service_rate; let exit_byte_rate = rate_pack.exit_byte_rate; let exit_service_rate = rate_pack.exit_service_rate; + let pending_payable_sec = scan_intervals.pending_payable_scan_interval.as_secs(); let payable_sec = scan_intervals.payable_scan_interval.as_secs(); let receivable_sec = scan_intervals.receivable_scan_interval.as_secs(); let threshold_interval_sec = payment_thresholds.threshold_interval_sec; @@ -652,6 +653,7 @@ impl Configurator { start_block_opt, scan_intervals: UiScanIntervals { payable_sec, + pending_payable_sec, receivable_sec, }, }; @@ -2590,6 +2592,7 @@ mod tests { start_block_opt: Some(3456), scan_intervals: UiScanIntervals { payable_sec: 125, + pending_payable_sec: 122, receivable_sec: 128 } } @@ -2608,7 +2611,7 @@ mod tests { })) .scan_intervals_result(Ok(ScanIntervals { payable_scan_interval: Duration::from_secs(125), - pending_payable_scan_interval: Duration::from_secs(30), + pending_payable_scan_interval: Duration::from_secs(122), receivable_scan_interval: Duration::from_secs(128), })) .payment_thresholds_result(Ok(PaymentThresholds { @@ -2720,6 +2723,7 @@ mod tests { start_block_opt: Some(3456), scan_intervals: UiScanIntervals { payable_sec: 125, + pending_payable_sec: 122, receivable_sec: 128 } } @@ -2812,6 +2816,7 @@ mod tests { start_block_opt: Some(3456), scan_intervals: UiScanIntervals { payable_sec: 0, + pending_payable_sec: 0, receivable_sec: 0 } } diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index 878484a25..f1f174e6e 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -3,7 +3,7 @@ use crate::accountant::db_access_objects::banned_dao::BannedDaoFactory; use crate::accountant::db_access_objects::payable_dao::PayableDaoFactory; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDaoFactory; use crate::accountant::db_access_objects::receivable_dao::ReceivableDaoFactory; -use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; use crate::accountant::{ checked_conversion, Accountant, ReceivedPayments, ReportTransactionReceipts, ScanError, SentPayables, diff --git a/node/src/sub_lib/blockchain_bridge.rs b/node/src/sub_lib/blockchain_bridge.rs index a6f3f2364..84aaabe48 100644 --- a/node/src/sub_lib/blockchain_bridge.rs +++ b/node/src/sub_lib/blockchain_bridge.rs @@ -1,8 +1,8 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::payable_scanner::blockchain_agent::BlockchainAgent; -use crate::accountant::scanners::payable_scanner::msgs::QualifiedPayablesMessage; +use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::{RequestTransactionReceipts, ResponseSkeleton, SkeletonOptHolder}; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::sub_lib::peer_actors::BindMessage; diff --git a/node/src/sub_lib/utils.rs b/node/src/sub_lib/utils.rs index 5bd7a655a..bbef4d3ec 100644 --- a/node/src/sub_lib/utils.rs +++ b/node/src/sub_lib/utils.rs @@ -139,7 +139,7 @@ fn crash_request_analyzer( } } -pub trait NotifyLaterHandle +pub trait NotifyLaterHandle: Send where A: Actor>, { @@ -167,7 +167,7 @@ impl NotifyLaterHandleReal { impl NotifyLaterHandle for NotifyLaterHandleReal where - M: Message + 'static, + M: Message + 'static + Send, A: Actor> + Handler, { fn notify_later( diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 1221b7acb..e5950c2b6 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -913,7 +913,7 @@ pub mod unshared_test_utils { impl NotifyLaterHandle for NotifyLaterHandleMock where - M: Message + 'static + Clone, + M: Message + 'static + Clone + Send, A: Actor> + Handler, { fn notify_later<'a>( diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index b11fce618..af9a1e73d 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -1,8 +1,8 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. #![cfg(test)] -use crate::accountant::scanners::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::payable_scanner::msgs::QualifiedPayablesMessage; +use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::ReportTransactionReceipts; use crate::accountant::{ ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForNewPayables, diff --git a/node/src/test_utils/recorder_stop_conditions.rs b/node/src/test_utils/recorder_stop_conditions.rs index 92fefe6e3..137a4a17b 100644 --- a/node/src/test_utils/recorder_stop_conditions.rs +++ b/node/src/test_utils/recorder_stop_conditions.rs @@ -178,11 +178,11 @@ macro_rules! match_lazily_every_type_id{ } mod tests { - use crate::accountant::scanners::ScanType; use crate::accountant::{ResponseSkeleton, ScanError, ScanForNewPayables}; use crate::daemon::crash_notification::CrashNotification; use crate::sub_lib::peer_actors::{NewPublicIp, StartMessage}; use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; + use masq_lib::messages::ScanType; use std::any::TypeId; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::vec; From 513c9a728c16175e7bb1ea6ffd3d40a5a65f83c5 Mon Sep 17 00:00:00 2001 From: Bert Date: Tue, 6 May 2025 23:59:49 +0200 Subject: [PATCH 12/49] GH-602: save point before going into fully private scanner --- node/src/accountant/mod.rs | 145 ++++++++------- node/src/accountant/scanners/mod.rs | 174 +++++++++--------- .../accountant/scanners/scan_schedulers.rs | 60 ++---- .../src/accountant/scanners/scanners_utils.rs | 6 + node/src/actor_system_factory.rs | 8 +- node/src/bootstrapper.rs | 10 +- .../unprivileged_parse_args_configuration.rs | 18 +- node/src/test_utils/mod.rs | 2 +- 8 files changed, 210 insertions(+), 213 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index c88710758..7a77bd129 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -76,14 +76,13 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; -use crate::accountant::scanners::scan_schedulers::ScanSchedulers; +use crate::accountant::scanners::scan_schedulers::{AutomaticSchedulingAwareScanner, ScanSchedulers}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; pub const CRASH_KEY: &str = "ACCOUNTANT"; pub const DEFAULT_PENDING_TOO_LONG_SEC: u64 = 21_600; //6 hours pub struct Accountant { - suppress_initial_scans: bool, consuming_wallet_opt: Option, earning_wallet: Wallet, payable_dao: Box, @@ -195,12 +194,7 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, _msg: StartMessage, ctx: &mut Self::Context) -> Self::Result { - if self.suppress_initial_scans { - info!( - &self.logger, - "Started with --scans off; declining to begin database and blockchain scans" - ); - } else { + if self.scan_schedulers.automatic_scans_enabled { debug!( &self.logger, "Started with --scans on; starting database and blockchain scans" @@ -211,6 +205,11 @@ impl Handler for Accountant { ctx.notify(ScanForReceivables { response_skeleton_opt: None, }); + } else { + info!( + &self.logger, + "Started with --scans off; declining to begin database and blockchain scans" + ); } } } @@ -220,11 +219,7 @@ impl Handler for Accountant { fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { let response_skeleton_opt = msg.response_skeleton_opt; - if self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) { - self.scan_schedulers - .payable - .schedule_for_new_payable(ctx, response_skeleton_opt) - } + self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) } } @@ -233,9 +228,7 @@ impl Handler for Accountant { fn handle(&mut self, msg: ScanForNewPayables, _ctx: &mut Self::Context) -> Self::Result { let response_skeleton = msg.response_skeleton_opt; - if self.handle_request_of_scan_for_new_payable(response_skeleton) { - todo!() - }; + self.handle_request_of_scan_for_new_payable(response_skeleton) } } @@ -253,7 +246,6 @@ impl Handler for Accountant { fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); - self.scan_schedulers.receivable.schedule(ctx, None); } } @@ -269,11 +261,16 @@ impl Handler for Accountant { .as_ref() .expect("UIGateway is not bound") .try_send(node_to_ui_msg) - .expect("UIGateway is dead") + .expect("UIGateway is dead"); + todo!(); + // Externally triggered scans are not allowed to unwind beyond their own scope; + // only the very action of the specified scan is performed. + } else { + todo!(); + self.scan_schedulers + .payable + .schedule_for_new_payable(ctx, response_skeleton_opt) } - self.scan_schedulers - .payable - .schedule_for_new_payable(ctx, response_skeleton_opt) } UiScanResult::ChainedScannersUnfinished => self .scan_schedulers @@ -299,20 +296,37 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: SentPayables, ctx: &mut Self::Context) -> Self::Result { - if let Some(node_to_ui_msg) = self + match self .scanners .payable .finish_scan(msg, &self.logger) - .finished() - { - self.ui_message_sub_opt - .as_ref() - .expect("UIGateway is not bound") - .try_send(node_to_ui_msg) - .expect("UIGateway is dead"); + .finished() { + None => {todo!()//self.scan_schedulers.pending_payable.schedule(ctx, None) + } + // TODO merge after tested in and out + Some(node_to_ui_msg) if self.scan_schedulers.automatic_scans_enabled => { + todo!(); + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + } + + Some(node_to_ui_msg) => { + + todo!("Test externally triggered msgs don't chain it up...if not..."); + self.ui_message_sub_opt + .as_ref() + .expect("UIGateway is not bound") + .try_send(node_to_ui_msg) + .expect("UIGateway is dead"); + + // Externally triggered scans should not provoke a sequence spread by intervals, that is + // exclusive to automatic (scheduled respectively) scanning processes. + } + } - - self.scan_schedulers.pending_payable.schedule(ctx, None) } } @@ -470,17 +484,15 @@ impl Accountant { let payable_dao = dao_factories.payable_dao_factory.make(); let pending_payable_dao = dao_factories.pending_payable_dao_factory.make(); let receivable_dao = dao_factories.receivable_dao_factory.make(); - let (scan_schedulers, scan_schedulers_flags) = ScanSchedulers::new(scan_intervals); + let scan_schedulers= ScanSchedulers::new(scan_intervals, config.automatic_scans_enabled); let scanners = Scanners::new( dao_factories, Rc::new(payment_thresholds), config.when_pending_too_long_sec, Rc::clone(&financial_statistics), - scan_schedulers_flags, ); Accountant { - suppress_initial_scans: config.suppress_initial_scans, consuming_wallet_opt: config.consuming_wallet_opt.clone(), earning_wallet, payable_dao, @@ -877,7 +889,7 @@ impl Accountant { fn handle_request_of_scan_for_new_payable( &mut self, response_skeleton_opt: Option, - ) -> bool { + ) { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( @@ -885,6 +897,7 @@ impl Accountant { SystemTime::now(), response_skeleton_opt, &self.logger, + self.scan_schedulers.pending_payable_sequence_in_process, ), None => Err(BeginScanError::NoConsumingWalletFound), }; @@ -896,7 +909,6 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - false } Err(e) => { e.handle_error( @@ -906,10 +918,10 @@ impl Accountant { ); //TODO simplify after having been properly tested - if e == BeginScanError::NothingToProcess { - todo!() + if e == BeginScanError::NothingToProcess && response_skeleton_opt.is_none() { + todo!("schedule new scan...") } else { - false + todo!("Just hit me up") } } } @@ -950,7 +962,7 @@ impl Accountant { fn handle_request_of_scan_for_pending_payable( &mut self, response_skeleton_opt: Option, - ) -> bool { + ) { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_pending_payable_scan_guarded( @@ -958,6 +970,7 @@ impl Accountant { SystemTime::now(), response_skeleton_opt, &self.logger, + self.scan_schedulers.pending_payable_sequence_in_process ), None => Err(BeginScanError::NoConsumingWalletFound), }; @@ -969,8 +982,6 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - - false } Err(e) => { e.handle_error( @@ -979,7 +990,12 @@ impl Accountant { response_skeleton_opt.is_some(), ); - e == BeginScanError::NothingToProcess + // TODO hang on!! this can happen only with external triggers (if we get up here it + // means this was preceded by the NewPayable or the RetryPayable scans, both always + // producing at least one payment) + if e == BeginScanError::NothingToProcess { + todo!() + } } } } @@ -1003,12 +1019,18 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"), - Err(e) => e.handle_error( - &self.logger, - ScanType::Receivables, - response_skeleton_opt.is_some(), - ), - }; + Err(e) => { + e.handle_error( + &self.logger, + ScanType::Receivables, + response_skeleton_opt.is_some(), + ); + } + } + + if response_skeleton_opt.is_some(){ + todo!("schedule") + } } fn handle_externally_triggered_scan( @@ -1019,14 +1041,10 @@ impl Accountant { ) { match scan_type { ScanType::Payables => { - if self.handle_request_of_scan_for_new_payable(Some(response_skeleton)) { - todo!() - } + self.handle_request_of_scan_for_new_payable(Some(response_skeleton)) } ScanType::PendingPayables => { - if self.handle_request_of_scan_for_pending_payable(Some(response_skeleton)) { - todo!() - } + self.handle_request_of_scan_for_pending_payable(Some(response_skeleton)) } ScanType::Receivables => { self.handle_request_of_scan_for_receivable(Some(response_skeleton)) @@ -1330,9 +1348,9 @@ mod tests { result.scan_schedulers.receivable.interval, default_scan_intervals.receivable_scan_interval, ); + assert_eq!(result.scan_schedulers.automatic_scans_enabled, true); assert_eq!(result.consuming_wallet_opt, None); assert_eq!(result.earning_wallet, *DEFAULT_EARNING_WALLET); - assert_eq!(result.suppress_initial_scans, false); result .message_id_generator .as_any() @@ -1460,7 +1478,7 @@ mod tests { pending_payable_scan_interval: Duration::from_millis(2_000), receivable_scan_interval: Duration::from_millis(10_000), }); - config.suppress_initial_scans = true; + config.automatic_scans_enabled = false; let subject = AccountantBuilder::default() .bootstrapper_config(config) .config_dao( @@ -1553,7 +1571,7 @@ mod tests { fn external_scan_payables_request_does_not_schedule_scan_for_new_payables_if_payables_empty() { todo!("write me up"); let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - config.suppress_initial_scans = true; + config.automatic_scans_enabled = false; let fingerprint = PendingPayableFingerprint { rowid: 1234, timestamp: SystemTime::now(), @@ -1852,7 +1870,7 @@ mod tests { #[test] fn externally_triggered_scan_pending_payables_request() { let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - config.suppress_initial_scans = true; + config.automatic_scans_enabled = false; config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), receivable_scan_interval: Duration::from_millis(10_000), @@ -1911,7 +1929,7 @@ mod tests { let test_name = "externally_triggered_scan_is_not_handled_in_case_the_scan_is_already_running"; let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - config.suppress_initial_scans = true; + config.automatic_scans_enabled = false; let fingerprint = PendingPayableFingerprint { rowid: 1234, timestamp: SystemTime::now(), @@ -2672,7 +2690,7 @@ mod tests { pending_payable_scan_interval: Duration::from_millis(50), receivable_scan_interval: Duration::from_millis(100), }); - config.suppress_initial_scans = true; + config.automatic_scans_enabled = false; let peer_actors = peer_actors_builder().build(); let subject = AccountantBuilder::default() .bootstrapper_config(config) @@ -2693,7 +2711,7 @@ mod tests { } #[test] - fn scan_for_payables_message_does_not_trigger_payment_for_balances_below_the_curve() { + fn scan_for_new_payables_does_not_trigger_payment_for_balances_below_the_curve() { init_test_logging(); let consuming_wallet = make_paying_wallet(b"consuming wallet"); let payment_thresholds = PaymentThresholds { @@ -2746,7 +2764,7 @@ mod tests { .non_pending_payables_result(vec![]); let (blockchain_bridge, _, blockchain_bridge_recordings_arc) = make_recorder(); let system = System::new( - "scan_for_payable_message_does_not_trigger_payment_for_balances_below_the_curve", + "scan_for_new_payables_does_not_trigger_payment_for_balances_below_the_curve", ); let blockchain_bridge_addr: Addr = blockchain_bridge.start(); let outbound_payments_instructions_sub = @@ -2762,6 +2780,7 @@ mod tests { SystemTime::now(), None, &subject.logger, + false ); System::current().stop(); diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 0711f9228..a93d2ba84 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -18,7 +18,7 @@ use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ separate_errors, separate_rowids_and_hashes, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata, }; -use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport}; +use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport, PendingPayableScanResult}; use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; use crate::accountant::{PendingPayableId, ScanForPendingPayables, ScanForRetryPayables}; use crate::accountant::{ @@ -46,29 +46,29 @@ use std::marker::PhantomData; use std::rc::Rc; use std::sync::{Arc, RwLock}; use std::time::{SystemTime}; +use futures::future::result; use time::format_description::parse; use time::OffsetDateTime; use web3::types::H256; use crate::accountant::scanners::payable_scanner_extension::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; -use crate::accountant::scanners::scan_schedulers::{ScanSchedulers, ScanSchedulersFlags}; +use crate::accountant::scanners::scan_schedulers::{ScanSchedulers}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::db_config::persistent_configuration::{PersistentConfiguration, PersistentConfigurationReal}; pub struct Scanners { - pub payable: Box>, - pub pending_payable: Box< + payable: Box>, + pending_payable: Box< dyn StartableAccessibleScanner< ScanForPendingPayables, RequestTransactionReceipts, ReportTransactionReceipts, >, >, - pub receivable: Box< + receivable: Box< dyn StartableAccessibleScanner, >, - scan_schedulers_flags: Rc>, } impl Scanners { @@ -77,7 +77,6 @@ impl Scanners { payment_thresholds: Rc, when_pending_too_long_sec: u64, financial_statistics: Rc>, - scan_schedulers_flags: Rc>, ) -> Self { let payable = Box::new(PayableScanner::new( dao_factories.payable_dao_factory.make(), @@ -109,76 +108,23 @@ impl Scanners { payable, pending_payable, receivable, - scan_schedulers_flags, } } - pub fn start_pending_payable_scan_guarded( - &mut self, - wallet: &Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, - ) -> Result { - let triggered_manually = response_skeleton_opt.is_some(); - if triggered_manually { - if !self - .scan_schedulers_flags - .borrow() - .pending_payable_sequence_is_ongoing - { - todo!() // ManualTriggerError - } - } - - match ( - self.pending_payable.scan_started_at(), - self.payable.scan_started_at(), - ) { - // TODO this might be possible!!! - (Some(pp_timestamp), Some(p_timestamp)) => unreachable!( - "Both payable scanners should never be allowed to run in parallel. Scan for \ - pending payables started at: {}, scan for payables started at: {}", - BeginScanError::timestamp_as_string(pp_timestamp), - BeginScanError::timestamp_as_string(p_timestamp) - ), - (Some(started_at), None) => { - return Err(BeginScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::PendingPayables, - started_at, - }) - } - (None, Some(started_at)) => { - return Err(BeginScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::Payables, - started_at, - }) - } - (None, None) => (), - } - self.pending_payable.scan_starter().scanner.start_scan( - wallet, - timestamp, - response_skeleton_opt, - logger, - ) - } - pub fn start_new_payable_scan_guarded( &mut self, wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, + pending_payable_sequence_in_process: bool ) -> Result { + let triggered_manually = response_skeleton_opt.is_some(); // Under normal circumstances, it is guaranteed that the new-payable scanner will never // overlap with the execution of the pending payable scanner, as they are safely // and automatically scheduled to run sequentially. However, since we allow for unexpected, // manually triggered scans, a conflict is possible and must be handled. - if self - .scan_schedulers_flags - .borrow() - .pending_payable_sequence_is_ongoing + if triggered_manually && pending_payable_sequence_in_process { todo!() // ManualTriggerError } @@ -198,6 +144,8 @@ impl Scanners { .start_scan(wallet, timestamp, response_skeleton_opt, logger) } + // Note: This scanner cannot be started on its own, it always comes after the pending payable + // scan, but only if it was made clear that there is a need to perform this retry. pub fn start_retry_payable_scan_guarded( &mut self, wallet: &Wallet, @@ -206,10 +154,10 @@ impl Scanners { logger: &Logger, ) -> Result { if let Some(started_at) = self.payable.scan_started_at() { - //TODO Oh god, this won't work with the externally triggered scanners!!! unreachable!( - "Guard for pending payables should've prevented running the tandem of scanners \ - if the payable scanner was still running. It started {} and is still running at {}", + "Guards should ensure that no payable scanner can run if the pending payable \ + repetitive sequence is still ongoing. However, some other payable scan intruded \ + at {} and is still running at {}", BeginScanError::timestamp_as_string(started_at), BeginScanError::timestamp_as_string(SystemTime::now()) ) @@ -223,6 +171,56 @@ impl Scanners { .start_scan(wallet, timestamp, response_skeleton_opt, logger) } + pub fn start_pending_payable_scan_guarded( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + pending_payable_sequence_in_process: bool + ) -> Result { + let triggered_manually = response_skeleton_opt.is_some(); + if triggered_manually && pending_payable_sequence_in_process + { + todo!() // ManualTriggerError + + } + + match ( + self.pending_payable.scan_started_at(), + self.payable.scan_started_at(), + ) { + + (Some(pp_timestamp), Some(p_timestamp)) => + // If you're wondering, then yes, this should be sacre in the relationship between + // Pending Payable and New Payable. + unreachable!("Both payable scanners should never be allowed to run in parallel. \ + Scan for pending payables started at: {}, scan for payables started at: {}", + BeginScanError::timestamp_as_string(pp_timestamp), + BeginScanError::timestamp_as_string(p_timestamp) + ), + (Some(started_at), None) => { + return Err(BeginScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::PendingPayables, + started_at, + }) + } + (None, Some(started_at)) => { + return Err(BeginScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Payables, + started_at, + }) + } + (None, None) => (), + } + self.pending_payable.scan_starter().scanner.start_scan( + wallet, + timestamp, + response_skeleton_opt, + logger, + ) + } + pub fn start_receivable_scan_guarded( &mut self, wallet: &Wallet, @@ -243,6 +241,18 @@ impl Scanners { logger, ) } + + pub fn finish_payable_scanner(&mut self, msg: SentPayables, logger: &Logger)-> Option{ + todo!() + } + + pub fn finish_pending_payable_scanner(&self, msg: ReportTransactionReceipts, logger: &Logger)-> PendingPayableScanResult{ + todo!() + } + + pub fn finish_receivable_scanner(&mut self, msg: ReceivedPayments, logger: &Logger) -> Option{ + todo!() + } } pub trait StartableAccessibleScanner: @@ -289,7 +299,7 @@ impl UiScanResult { } } -// Using this Access token to screen away a private interface from its public counterpart, because +// Using this access token to screen away the private interface from its public counterpart, because // Rust doesn't allow selective private methods within a public trait pub struct PrivateScanStarter<'s, TriggerMessage, StartMessage> { phantom: PhantomData, @@ -1657,7 +1667,6 @@ mod tests { use web3::Error; use masq_lib::messages::ScanType; use crate::accountant::scanners::local_test_utils::NullScanner; - use crate::accountant::scanners::scan_schedulers::ScanSchedulersFlags; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; #[test] @@ -1684,7 +1693,6 @@ mod tests { let payment_thresholds = make_custom_payment_thresholds(); let payment_thresholds_rc = Rc::new(payment_thresholds); let initial_rc_count = Rc::strong_count(&payment_thresholds_rc); - let scan_schedulers_flags = Rc::new(RefCell::new(ScanSchedulersFlags::default())); let mut scanners = Scanners::new( DaoFactories { @@ -1697,7 +1705,6 @@ mod tests { Rc::clone(&payment_thresholds_rc), when_pending_too_long_sec, Rc::new(RefCell::new(financial_statistics.clone())), - Rc::clone(&scan_schedulers_flags), ); let payable_scanner = scanners @@ -1758,23 +1765,6 @@ mod tests { Rc::strong_count(&payment_thresholds_rc), initial_rc_count + 3 ); - assert_eq!( - scanners - .scan_schedulers_flags - .borrow() - .pending_payable_sequence_is_ongoing, - false - ); - scan_schedulers_flags - .borrow_mut() - .pending_payable_sequence_is_ongoing = true; - assert_eq!( - scanners - .scan_schedulers_flags - .borrow() - .pending_payable_sequence_is_ongoing, - true - ); } #[test] @@ -1798,6 +1788,7 @@ mod tests { now, None, &Logger::new(test_name), + false ); let timestamp = subject.payable.scan_started_at(); @@ -1839,6 +1830,7 @@ mod tests { previous_scan_started_at, None, &Logger::new("test"), + false ); let result = subject.start_new_payable_scan_guarded( @@ -1846,6 +1838,7 @@ mod tests { SystemTime::now(), None, &Logger::new("test"), + false ); let is_scan_running = subject.payable.scan_started_at().is_some(); @@ -2934,6 +2927,7 @@ mod tests { now, None, &Logger::new(test_name), + false ); let no_of_pending_payables = fingerprints.len(); @@ -2968,13 +2962,14 @@ mod tests { let payable_scanner = PayableScannerBuilder::new().build(); subject.payable = Box::new(payable_scanner); let logger = Logger::new("test"); - let _ = subject.start_pending_payable_scan_guarded(&consuming_wallet, now, None, &logger); + let _ = subject.start_pending_payable_scan_guarded(&consuming_wallet, now, None, &logger, false); let result = subject.start_pending_payable_scan_guarded( &consuming_wallet, SystemTime::now(), None, &logger, + false ); let is_scan_running = subject.pending_payable.scan_started_at().is_some(); @@ -3005,6 +3000,7 @@ mod tests { SystemTime::now(), None, &logger, + false ); let is_scan_running = subject.pending_payable.scan_started_at().is_some(); @@ -3043,6 +3039,7 @@ mod tests { SystemTime::now(), None, &Logger::new("test"), + false ); })) .unwrap_err(); @@ -4230,7 +4227,6 @@ mod tests { payable: Box::new(NullScanner::new()), pending_payable: Box::new(NullScanner::new()), receivable: Box::new(NullScanner::new()), - scan_schedulers_flags: Rc::new(RefCell::new(ScanSchedulersFlags::default())), } } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index c4f7f6547..74ff7b4c5 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -19,34 +19,26 @@ pub struct ScanSchedulers { pub payable: PayableScanScheduler, pub pending_payable: SimplePeriodicalScanScheduler, pub receivable: SimplePeriodicalScanScheduler, - pub scan_schedulers_flags: Rc>, + pub pending_payable_sequence_in_process: bool, + pub automatic_scans_enabled: bool } impl ScanSchedulers { - pub fn new(scan_intervals: ScanIntervals) -> (Self, Rc>) { - let scan_schedulers_flags = Rc::new(RefCell::new(ScanSchedulersFlags::default())); - - ( - Self { - payable: PayableScanScheduler::new(scan_intervals.payable_scan_interval), - pending_payable: SimplePeriodicalScanScheduler::new( - scan_intervals.pending_payable_scan_interval, - ), - receivable: SimplePeriodicalScanScheduler::new( - scan_intervals.receivable_scan_interval, - ), - scan_schedulers_flags: Rc::clone(&scan_schedulers_flags), - }, - scan_schedulers_flags, - ) + pub fn new(scan_intervals: ScanIntervals, automatic_scans_enabled: bool) -> Self { + Self { + payable: PayableScanScheduler::new(scan_intervals.payable_scan_interval), + pending_payable: SimplePeriodicalScanScheduler::new( + scan_intervals.pending_payable_scan_interval, + ), + receivable: SimplePeriodicalScanScheduler::new( + scan_intervals.receivable_scan_interval, + ), + pending_payable_sequence_in_process: false, + automatic_scans_enabled, + } } } -#[derive(Default)] -pub struct ScanSchedulersFlags { - pub pending_payable_sequence_is_ongoing: bool, -} - #[derive(Debug, PartialEq)] pub enum PayableScanSchedulerError { ScanForNewPayableAlreadyScheduled, @@ -277,8 +269,9 @@ mod tests { pending_payable_scan_interval: Duration::from_secs(2), receivable_scan_interval: Duration::from_secs(7), }; + let automatic_scans_enabled = true; - let (schedulers, schedulers_flags) = ScanSchedulers::new(scan_intervals); + let schedulers = ScanSchedulers::new(scan_intervals, automatic_scans_enabled); assert_eq!( schedulers.payable.nominal_interval, @@ -317,27 +310,10 @@ mod tests { ); assert_eq!( schedulers - .scan_schedulers_flags - .borrow() - .pending_payable_sequence_is_ongoing, + .pending_payable_sequence_in_process, false ); - assert_eq!( - schedulers_flags - .borrow() - .pending_payable_sequence_is_ongoing, - false - ); - schedulers - .scan_schedulers_flags - .borrow_mut() - .pending_payable_sequence_is_ongoing = true; - assert_eq!( - schedulers_flags - .borrow() - .pending_payable_sequence_is_ongoing, - true - ); + assert_eq!(schedulers.automatic_scans_enabled, true) } #[test] diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 26707c9be..9a56130d9 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -313,6 +313,7 @@ pub mod pending_payable_scanner_utils { use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use masq_lib::logger::Logger; use std::time::SystemTime; + use masq_lib::ui_gateway::NodeToUiMessage; #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct PendingPayableScanReport { @@ -326,6 +327,11 @@ pub mod pending_payable_scanner_utils { !self.still_pending.is_empty() || !self.failures.is_empty() } } + + pub enum PendingPayableScanResult { + PendingPayablesFinished(Option), + PaymentRetryRequired + } pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { timestamp diff --git a/node/src/actor_system_factory.rs b/node/src/actor_system_factory.rs index 20ee55ed2..0b26fd8e4 100644 --- a/node/src/actor_system_factory.rs +++ b/node/src/actor_system_factory.rs @@ -1166,7 +1166,7 @@ mod tests { crash_point: CrashPoint::None, dns_servers: vec![], scan_intervals_opt: Some(ScanIntervals::default()), - suppress_initial_scans: false, + automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1241,7 +1241,7 @@ mod tests { crash_point: CrashPoint::None, dns_servers: vec![], scan_intervals_opt: None, - suppress_initial_scans: false, + automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1544,7 +1544,7 @@ mod tests { crash_point: CrashPoint::None, dns_servers: vec![], scan_intervals_opt: None, - suppress_initial_scans: false, + automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { @@ -1730,7 +1730,7 @@ mod tests { crash_point: CrashPoint::None, dns_servers: vec![], scan_intervals_opt: None, - suppress_initial_scans: false, + automatic_scans_enabled: true, clandestine_discriminator_factories: Vec::new(), ui_gateway_config: UiGatewayConfig { ui_port: 5335 }, blockchain_bridge_config: BlockchainBridgeConfig { diff --git a/node/src/bootstrapper.rs b/node/src/bootstrapper.rs index 2ef265e69..6aed3bfbe 100644 --- a/node/src/bootstrapper.rs +++ b/node/src/bootstrapper.rs @@ -335,7 +335,7 @@ pub struct BootstrapperConfig { pub log_level: LevelFilter, pub dns_servers: Vec, pub scan_intervals_opt: Option, - pub suppress_initial_scans: bool, + pub automatic_scans_enabled: bool, pub when_pending_too_long_sec: u64, pub crash_point: CrashPoint, pub clandestine_discriminator_factories: Vec>, @@ -371,7 +371,7 @@ impl BootstrapperConfig { log_level: LevelFilter::Off, dns_servers: vec![], scan_intervals_opt: None, - suppress_initial_scans: false, + automatic_scans_enabled: true, crash_point: CrashPoint::None, clandestine_discriminator_factories: vec![], ui_gateway_config: UiGatewayConfig { @@ -415,7 +415,7 @@ impl BootstrapperConfig { self.consuming_wallet_opt = unprivileged.consuming_wallet_opt; self.db_password_opt = unprivileged.db_password_opt; self.scan_intervals_opt = unprivileged.scan_intervals_opt; - self.suppress_initial_scans = unprivileged.suppress_initial_scans; + self.automatic_scans_enabled = unprivileged.automatic_scans_enabled; self.payment_thresholds_opt = unprivileged.payment_thresholds_opt; self.when_pending_too_long_sec = unprivileged.when_pending_too_long_sec; } @@ -1250,7 +1250,7 @@ mod tests { unprivileged_config.consuming_wallet_opt = consuming_wallet_opt.clone(); unprivileged_config.db_password_opt = db_password_opt.clone(); unprivileged_config.scan_intervals_opt = Some(ScanIntervals::default()); - unprivileged_config.suppress_initial_scans = false; + unprivileged_config.automatic_scans_enabled = true; unprivileged_config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; privileged_config.merge_unprivileged(unprivileged_config); @@ -1275,7 +1275,7 @@ mod tests { privileged_config.scan_intervals_opt, Some(ScanIntervals::default()) ); - assert_eq!(privileged_config.suppress_initial_scans, false); + assert_eq!(privileged_config.automatic_scans_enabled, true); assert_eq!( privileged_config.when_pending_too_long_sec, DEFAULT_PENDING_TOO_LONG_SEC diff --git a/node/src/node_configurator/unprivileged_parse_args_configuration.rs b/node/src/node_configurator/unprivileged_parse_args_configuration.rs index 33b38fa5a..66c3c5247 100644 --- a/node/src/node_configurator/unprivileged_parse_args_configuration.rs +++ b/node/src/node_configurator/unprivileged_parse_args_configuration.rs @@ -504,12 +504,12 @@ fn configure_accountant_config( |pc: &dyn PersistentConfiguration| pc.scan_intervals(), |pc: &mut dyn PersistentConfiguration, intervals| pc.set_scan_intervals(intervals), )?; - let suppress_initial_scans = + let automatic_scans_enabled = value_m!(multi_config, "scans", String).unwrap_or_else(|| "on".to_string()) == *"off"; config.payment_thresholds_opt = Some(payment_thresholds); config.scan_intervals_opt = Some(scan_intervals); - config.suppress_initial_scans = suppress_initial_scans; + config.automatic_scans_enabled = automatic_scans_enabled; config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; Ok(()) } @@ -1872,7 +1872,7 @@ mod tests { Some(expected_payment_thresholds) ); assert_eq!(config.scan_intervals_opt, Some(expected_scan_intervals)); - assert_eq!(config.suppress_initial_scans, false); + assert_eq!(config.automatic_scans_enabled, true); assert_eq!( config.when_pending_too_long_sec, DEFAULT_PENDING_TOO_LONG_SEC @@ -1939,7 +1939,7 @@ mod tests { pending_payable_scan_interval: Duration::from_secs(15), receivable_scan_interval: Duration::from_secs(130), }; - let expected_suppress_initial_scans = false; + let expected_automatic_scans_enabled = true; let expected_when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; assert_eq!( config.payment_thresholds_opt, @@ -1947,8 +1947,8 @@ mod tests { ); assert_eq!(config.scan_intervals_opt, Some(expected_scan_intervals)); assert_eq!( - config.suppress_initial_scans, - expected_suppress_initial_scans + config.automatic_scans_enabled, + expected_automatic_scans_enabled ); assert_eq!( config.when_pending_too_long_sec, @@ -2578,7 +2578,7 @@ mod tests { ) .unwrap(); - assert_eq!(bootstrapper_config.suppress_initial_scans, true); + assert_eq!(bootstrapper_config.automatic_scans_enabled, false); } #[test] @@ -2599,7 +2599,7 @@ mod tests { ) .unwrap(); - assert_eq!(bootstrapper_config.suppress_initial_scans, false); + assert_eq!(bootstrapper_config.automatic_scans_enabled, true); } #[test] @@ -2620,7 +2620,7 @@ mod tests { ) .unwrap(); - assert_eq!(bootstrapper_config.suppress_initial_scans, false); + assert_eq!(bootstrapper_config.automatic_scans_enabled, true); } fn make_persistent_config( diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index e5950c2b6..1b107a2eb 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -683,7 +683,7 @@ pub mod unshared_test_utils { pub fn make_bc_with_defaults() -> BootstrapperConfig { let mut config = BootstrapperConfig::new(); config.scan_intervals_opt = Some(ScanIntervals::default()); - config.suppress_initial_scans = false; + config.automatic_scans_enabled = true; config.when_pending_too_long_sec = DEFAULT_PENDING_TOO_LONG_SEC; config.payment_thresholds_opt = Some(PaymentThresholds::default()); config From 755a45934b3cb3520ffb7870456b253636907c95 Mon Sep 17 00:00:00 2001 From: Bert Date: Thu, 8 May 2025 22:14:33 +0200 Subject: [PATCH 13/49] GH-602: another save point, this time before turning the scanner into fully private object --- node/src/accountant/mod.rs | 70 ++- node/src/accountant/scanners/mod.rs | 451 ++++++++++++------ .../scanners/payable_scanner_extension/mod.rs | 21 +- .../accountant/scanners/scan_schedulers.rs | 12 +- .../src/accountant/scanners/scanners_utils.rs | 6 +- 5 files changed, 357 insertions(+), 203 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 7a77bd129..ef6575a62 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -77,6 +77,7 @@ use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; use crate::accountant::scanners::scan_schedulers::{AutomaticSchedulingAwareScanner, ScanSchedulers}; +use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; pub const CRASH_KEY: &str = "ACCOUNTANT"; @@ -254,8 +255,8 @@ impl Handler for Accountant { fn handle(&mut self, msg: ReportTransactionReceipts, ctx: &mut Self::Context) -> Self::Result { let response_skeleton_opt = msg.response_skeleton_opt; - match self.scanners.pending_payable.finish_scan(msg, &self.logger) { - UiScanResult::Finished(ui_msg_opt) => { + match self.scanners.finish_pending_payable_scan(msg, &self.logger) { + PendingPayableScanResult::PendingPayablesFinished(ui_msg_opt) => { if let Some(node_to_ui_msg) = ui_msg_opt { self.ui_message_sub_opt .as_ref() @@ -263,8 +264,8 @@ impl Handler for Accountant { .try_send(node_to_ui_msg) .expect("UIGateway is dead"); todo!(); - // Externally triggered scans are not allowed to unwind beyond their own scope; - // only the very action of the specified scan is performed. + // Externally triggered scans are not allowed to unwind beyond their own scope; + // only the very action of the specified scan is performed. } else { todo!(); self.scan_schedulers @@ -272,7 +273,7 @@ impl Handler for Accountant { .schedule_for_new_payable(ctx, response_skeleton_opt) } } - UiScanResult::ChainedScannersUnfinished => self + PendingPayableScanResult::PaymentRetryRequired => self .scan_schedulers .payable .schedule_for_retry_payable(ctx, response_skeleton_opt), @@ -296,12 +297,9 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: SentPayables, ctx: &mut Self::Context) -> Self::Result { - match self - .scanners - .payable - .finish_scan(msg, &self.logger) - .finished() { - None => {todo!()//self.scan_schedulers.pending_payable.schedule(ctx, None) + match self.scanners.finish_payable_scan(msg, &self.logger) { + None => { + todo!() //self.scan_schedulers.pending_payable.schedule(ctx, None) } // TODO merge after tested in and out Some(node_to_ui_msg) if self.scan_schedulers.automatic_scans_enabled => { @@ -312,20 +310,18 @@ impl Handler for Accountant { .try_send(node_to_ui_msg) .expect("UIGateway is dead"); } - - Some(node_to_ui_msg) => { - + + Some(node_to_ui_msg) => { todo!("Test externally triggered msgs don't chain it up...if not..."); self.ui_message_sub_opt .as_ref() .expect("UIGateway is not bound") .try_send(node_to_ui_msg) .expect("UIGateway is dead"); - + // Externally triggered scans should not provoke a sequence spread by intervals, that is // exclusive to automatic (scheduled respectively) scanning processes. } - } } } @@ -334,12 +330,7 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ReceivedPayments, _ctx: &mut Self::Context) -> Self::Result { - if let Some(node_to_ui_msg) = self - .scanners - .receivable - .finish_scan(msg, &self.logger) - .finished() - { + if let Some(node_to_ui_msg) = self.scanners.finish_receivable_scan(msg, &self.logger) { self.ui_message_sub_opt .as_ref() .expect("UIGateway is not bound") @@ -354,17 +345,18 @@ impl Handler for Accountant { fn handle(&mut self, scan_error: ScanError, _ctx: &mut Self::Context) -> Self::Result { error!(self.logger, "Received ScanError: {:?}", scan_error); - match scan_error.scan_type { - ScanType::Payables => { - self.scanners.payable.mark_as_ended(&self.logger); - } - ScanType::PendingPayables => { - self.scanners.pending_payable.mark_as_ended(&self.logger); - } - ScanType::Receivables => { - self.scanners.receivable.mark_as_ended(&self.logger); - } - }; + self.scanners.scan_error_scan_reset(&scan_error); + // match scan_error.scan_type { + // ScanType::Payables => { + // self.scanners.payable.mark_as_ended(&self.logger); + // } + // ScanType::PendingPayables => { + // self.scanners.pending_payable.mark_as_ended(&self.logger); + // } + // ScanType::Receivables => { + // self.scanners.receivable.mark_as_ended(&self.logger); + // } + // }; if let Some(response_skeleton) = scan_error.response_skeleton_opt { let error_msg = NodeToUiMessage { target: ClientId(response_skeleton.client_id), @@ -484,7 +476,7 @@ impl Accountant { let payable_dao = dao_factories.payable_dao_factory.make(); let pending_payable_dao = dao_factories.pending_payable_dao_factory.make(); let receivable_dao = dao_factories.receivable_dao_factory.make(); - let scan_schedulers= ScanSchedulers::new(scan_intervals, config.automatic_scans_enabled); + let scan_schedulers = ScanSchedulers::new(scan_intervals, config.automatic_scans_enabled); let scanners = Scanners::new( dao_factories, Rc::new(payment_thresholds), @@ -970,7 +962,7 @@ impl Accountant { SystemTime::now(), response_skeleton_opt, &self.logger, - self.scan_schedulers.pending_payable_sequence_in_process + self.scan_schedulers.pending_payable_sequence_in_process, ), None => Err(BeginScanError::NoConsumingWalletFound), }; @@ -994,7 +986,7 @@ impl Accountant { // means this was preceded by the NewPayable or the RetryPayable scans, both always // producing at least one payment) if e == BeginScanError::NothingToProcess { - todo!() + todo!() } } } @@ -1027,8 +1019,8 @@ impl Accountant { ); } } - - if response_skeleton_opt.is_some(){ + + if response_skeleton_opt.is_some() { todo!("schedule") } } @@ -2780,7 +2772,7 @@ mod tests { SystemTime::now(), None, &subject.logger, - false + false, ); System::current().stop(); diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index a93d2ba84..52e543ac1 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -20,7 +20,7 @@ use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ }; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport, PendingPayableScanResult}; use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; -use crate::accountant::{PendingPayableId, ScanForPendingPayables, ScanForRetryPayables}; +use crate::accountant::{PendingPayableId, ScanError, ScanForPendingPayables, ScanForRetryPayables}; use crate::accountant::{ comma_joined_stringifiable, gwei_to_wei, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForNewPayables, @@ -57,18 +57,17 @@ use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_le use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::db_config::persistent_configuration::{PersistentConfiguration, PersistentConfigurationReal}; +// Keep the individual scanner objects private! pub struct Scanners { - payable: Box>, + payable: Box, pending_payable: Box< - dyn StartableAccessibleScanner< + dyn PrivateScanner< ScanForPendingPayables, RequestTransactionReceipts, ReportTransactionReceipts, >, >, - receivable: Box< - dyn StartableAccessibleScanner, - >, + receivable: Box>, } impl Scanners { @@ -117,34 +116,37 @@ impl Scanners { timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - pending_payable_sequence_in_process: bool + pending_payable_sequence_in_process: bool, ) -> Result { let triggered_manually = response_skeleton_opt.is_some(); // Under normal circumstances, it is guaranteed that the new-payable scanner will never // overlap with the execution of the pending payable scanner, as they are safely // and automatically scheduled to run sequentially. However, since we allow for unexpected, // manually triggered scans, a conflict is possible and must be handled. - if triggered_manually && pending_payable_sequence_in_process - { + if triggered_manually && pending_payable_sequence_in_process { todo!() // ManualTriggerError } - if let Some(started_at) = self.payable.scan_started_at() { + let scanner = self.payable.access_scanner().scanner; + + if let Some(started_at) = scanner.scan_started_at() { return Err(BeginScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Payables, started_at, }); } - let starter: PrivateScanStarter = - self.payable.scan_starter(); + let starter: PrivateStartableScannerAccessToken< + ScanForNewPayables, + QualifiedPayablesMessage, + > = self.payable.access_startable_scanner(); starter .scanner .start_scan(wallet, timestamp, response_skeleton_opt, logger) } - // Note: This scanner cannot be started on its own, it always comes after the pending payable + // Note: This scanner cannot be started on its own, it always comes after the pending payable // scan, but only if it was made clear that there is a need to perform this retry. pub fn start_retry_payable_scan_guarded( &mut self, @@ -153,7 +155,8 @@ impl Scanners { response_skeleton_opt: Option, logger: &Logger, ) -> Result { - if let Some(started_at) = self.payable.scan_started_at() { + let scanner = self.payable.access_scanner().scanner; + if let Some(started_at) = scanner.scan_started_at() { unreachable!( "Guards should ensure that no payable scanner can run if the pending payable \ repetitive sequence is still ongoing. However, some other payable scan intruded \ @@ -163,8 +166,10 @@ impl Scanners { ) } - let starter: PrivateScanStarter = - self.payable.scan_starter(); + let starter: PrivateStartableScannerAccessToken< + ScanForRetryPayables, + QualifiedPayablesMessage, + > = self.payable.access_startable_scanner(); starter .scanner @@ -177,28 +182,30 @@ impl Scanners { timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - pending_payable_sequence_in_process: bool + pending_payable_sequence_in_process: bool, ) -> Result { let triggered_manually = response_skeleton_opt.is_some(); - if triggered_manually && pending_payable_sequence_in_process - { - todo!() // ManualTriggerError - + if triggered_manually && pending_payable_sequence_in_process { + todo!() // ManualTriggerError } + let pending_payable_scanner = self.pending_payable.access_scanner().scanner; + let payable_scanner = self.payable.access_scanner().scanner; match ( - self.pending_payable.scan_started_at(), - self.payable.scan_started_at(), + pending_payable_scanner.scan_started_at(), + payable_scanner.scan_started_at(), ) { - (Some(pp_timestamp), Some(p_timestamp)) => - // If you're wondering, then yes, this should be sacre in the relationship between - // Pending Payable and New Payable. - unreachable!("Both payable scanners should never be allowed to run in parallel. \ + // If you're wondering, then yes, this should be sacre in the relationship between + // Pending Payable and New Payable. + { + unreachable!( + "Both payable scanners should never be allowed to run in parallel. \ Scan for pending payables started at: {}, scan for payables started at: {}", - BeginScanError::timestamp_as_string(pp_timestamp), - BeginScanError::timestamp_as_string(p_timestamp) - ), + BeginScanError::timestamp_as_string(pp_timestamp), + BeginScanError::timestamp_as_string(p_timestamp) + ) + } (Some(started_at), None) => { return Err(BeginScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::PendingPayables, @@ -213,12 +220,10 @@ impl Scanners { } (None, None) => (), } - self.pending_payable.scan_starter().scanner.start_scan( - wallet, - timestamp, - response_skeleton_opt, - logger, - ) + self.pending_payable + .access_startable_scanner() + .scanner + .start_scan(wallet, timestamp, response_skeleton_opt, logger) } pub fn start_receivable_scan_guarded( @@ -228,35 +233,67 @@ impl Scanners { response_skeleton_opt: Option, logger: &Logger, ) -> Result { - if let Some(started_at) = self.receivable.scan_started_at() { + let scanner = self.receivable.access_scanner().scanner; + if let Some(started_at) = scanner.scan_started_at() { return Err(BeginScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Receivables, started_at, }); } - self.receivable.scan_starter().scanner.start_scan( - wallet, - timestamp, - response_skeleton_opt, - logger, - ) + self.receivable + .access_startable_scanner() + .scanner + .start_scan(wallet, timestamp, response_skeleton_opt, logger) } - - pub fn finish_payable_scanner(&mut self, msg: SentPayables, logger: &Logger)-> Option{ + + pub fn finish_payable_scan( + &mut self, + msg: SentPayables, + logger: &Logger, + ) -> Option { todo!() } - - pub fn finish_pending_payable_scanner(&self, msg: ReportTransactionReceipts, logger: &Logger)-> PendingPayableScanResult{ + + pub fn finish_pending_payable_scan( + &self, + msg: ReportTransactionReceipts, + logger: &Logger, + ) -> PendingPayableScanResult { todo!() } - - pub fn finish_receivable_scanner(&mut self, msg: ReceivedPayments, logger: &Logger) -> Option{ + + pub fn finish_receivable_scan( + &mut self, + msg: ReceivedPayments, + logger: &Logger, + ) -> Option { + todo!() + } + + pub fn scan_error_scan_reset(&self, error: &ScanError) { + todo!() + } + + pub fn try_skipping_payable_adjustment( + &self, + msg: &BlockchainAgentWithContextMessage, + logger: &Logger, + ) -> Result, String> { + todo!() + } + + pub fn perform_payable_adjustment( + &self, + setup: PreparedAdjustment, + logger: &Logger, + ) -> OutboundPaymentsInstructions { todo!() } } -pub trait StartableAccessibleScanner: - ScanWithStarter + AccessibleScanner +trait PrivateScanner: + PrivateStartableScannerWithAccessToken + + PrivateScannerWithAccessToken where TriggerMessage: Message, StartMessage: Message, @@ -264,16 +301,22 @@ where { } -pub trait ScanWithStarter { - fn scan_starter(&mut self) -> PrivateScanStarter; +pub trait PrivateStartableScannerWithAccessToken { + fn access_startable_scanner( + &mut self, + ) -> PrivateStartableScannerAccessToken; } -pub trait AccessibleScanner +pub trait PrivateScannerWithAccessToken { + fn access_scanner(&mut self) -> PrivateScannerAccessToken; +} + +trait Scanner where - StartMessage: Message, EndMessage: Message, { - // fn scan_starter(starter: &mut dyn InaccessibleScanner) -> PrivateScanStarter where Self: Sized; + //TODO this is an old initiative, should go away + // fn access_scanner(starter: &mut dyn StartableScanner) -> PrivateScannerAccessToken where Self: Sized; fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> UiScanResult; fn scan_started_at(&self) -> Option; fn mark_as_started(&mut self, timestamp: SystemTime); @@ -283,6 +326,7 @@ where as_any_mut_in_trait!(); } +//TODO replace me with a generic possibly different for each scanner #[derive(Debug, PartialEq)] pub enum UiScanResult { Finished(Option), @@ -299,27 +343,36 @@ impl UiScanResult { } } +pub struct PrivateStartableScannerAccessToken<'s, TriggerMessage, StartMessage> { + // Strictly private + scanner: &'s mut dyn StartableScanner, +} + +impl<'s, TriggerMessage, StartMessage> + PrivateStartableScannerAccessToken<'s, TriggerMessage, StartMessage> +{ + fn new( + scanner: &'s mut dyn StartableScanner, + ) -> PrivateStartableScannerAccessToken<'s, TriggerMessage, StartMessage> { + Self { scanner } + } +} + // Using this access token to screen away the private interface from its public counterpart, because // Rust doesn't allow selective private methods within a public trait -pub struct PrivateScanStarter<'s, TriggerMessage, StartMessage> { - phantom: PhantomData, +pub struct PrivateScannerAccessToken<'s, EndMessage> { // Strictly private - scanner: &'s mut dyn InaccessibleScanner, + scanner: &'s mut dyn Scanner, } -impl<'s, TriggerMessage, StartMessage> PrivateScanStarter<'s, TriggerMessage, StartMessage> { - fn new( - scanner: &'s mut dyn InaccessibleScanner, - ) -> PrivateScanStarter<'s, TriggerMessage, StartMessage> { - Self { - phantom: PhantomData::default(), - scanner, - } +impl<'s, EndMessage> PrivateScannerAccessToken<'s, EndMessage> { + fn new(scanner: &'s mut dyn Scanner) -> PrivateScannerAccessToken<'s, EndMessage> { + Self { scanner } } } // Strictly private -trait InaccessibleScanner +trait StartableScanner where TriggerMessage: Message, StartMessage: Message, @@ -398,9 +451,9 @@ pub struct PayableScanner { pub payment_adjuster: Box, } -impl MultistageDualPayableScanner for PayableScanner {} +impl MultistageDualPayableScanner for PayableScanner {} -impl InaccessibleScanner for PayableScanner { +impl StartableScanner for PayableScanner { fn start_scan( &mut self, consuming_wallet: &Wallet, @@ -444,7 +497,7 @@ impl InaccessibleScanner for Payab } } -impl InaccessibleScanner for PayableScanner { +impl StartableScanner for PayableScanner { fn start_scan( &mut self, _consuming_wallet: &Wallet, @@ -456,21 +509,35 @@ impl InaccessibleScanner for Pay } } -impl ScanWithStarter for PayableScanner { - fn scan_starter(&mut self) -> PrivateScanStarter { - PrivateScanStarter::new(self) +impl PrivateStartableScannerWithAccessToken + for PayableScanner +{ + fn access_startable_scanner( + &mut self, + ) -> PrivateStartableScannerAccessToken { + todo!(); + PrivateStartableScannerAccessToken::new(self) } } -impl ScanWithStarter for PayableScanner { - fn scan_starter( +impl PrivateStartableScannerWithAccessToken + for PayableScanner +{ + fn access_startable_scanner( &mut self, - ) -> PrivateScanStarter { - PrivateScanStarter::new(self) + ) -> PrivateStartableScannerAccessToken { + PrivateStartableScannerAccessToken::new(self) + } +} + +impl PrivateScannerWithAccessToken for PayableScanner { + fn access_scanner(&mut self) -> PrivateScannerAccessToken { + todo!(); + PrivateScannerAccessToken::new(self) } } -impl AccessibleScanner for PayableScanner { +impl Scanner for PayableScanner { fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> UiScanResult { let (sent_payables, err_opt) = separate_errors(&message, logger); debug!( @@ -789,24 +856,31 @@ pub struct PendingPayableScanner { pub financial_statistics: Rc>, } -impl - StartableAccessibleScanner< - ScanForPendingPayables, - RequestTransactionReceipts, - ReportTransactionReceipts, - > for PendingPayableScanner +impl PrivateScanner + for PendingPayableScanner { } -impl ScanWithStarter for PendingPayableScanner { - fn scan_starter( +impl PrivateStartableScannerWithAccessToken + for PendingPayableScanner +{ + fn access_startable_scanner( &mut self, - ) -> PrivateScanStarter { - PrivateScanStarter::new(self) + ) -> PrivateStartableScannerAccessToken + { + todo!(); + PrivateStartableScannerAccessToken::new(self) + } +} + +impl PrivateScannerWithAccessToken for PendingPayableScanner { + fn access_scanner(&mut self) -> PrivateScannerAccessToken { + todo!(); + PrivateScannerAccessToken::new(self) } } -impl InaccessibleScanner +impl StartableScanner for PendingPayableScanner { fn start_scan( @@ -839,9 +913,7 @@ impl InaccessibleScanner } } -impl AccessibleScanner - for PendingPayableScanner -{ +impl Scanner for PendingPayableScanner { fn finish_scan(&mut self, message: ReportTransactionReceipts, logger: &Logger) -> UiScanResult { let construct_msg_scan_ended_to_ui = move || { message @@ -1052,18 +1124,30 @@ pub struct ReceivableScanner { pub financial_statistics: Rc>, } -impl StartableAccessibleScanner +impl PrivateScanner + for ReceivableScanner +{ +} + +impl PrivateStartableScannerWithAccessToken for ReceivableScanner { + fn access_startable_scanner( + &mut self, + ) -> PrivateStartableScannerAccessToken { + todo!(); + PrivateStartableScannerAccessToken::new(self) + } } -impl ScanWithStarter for ReceivableScanner { - fn scan_starter(&mut self) -> PrivateScanStarter { - PrivateScanStarter::new(self) +impl PrivateScannerWithAccessToken for ReceivableScanner { + fn access_scanner(&mut self) -> PrivateScannerAccessToken { + todo!(); + PrivateScannerAccessToken::new(self) } } -impl InaccessibleScanner for ReceivableScanner { +impl StartableScanner for ReceivableScanner { fn start_scan( &mut self, earning_wallet: &Wallet, @@ -1082,7 +1166,7 @@ impl InaccessibleScanner for Receivabl } } -impl AccessibleScanner for ReceivableScanner { +impl Scanner for ReceivableScanner { // fn start_scan( // &mut self, // earning_wallet: Wallet, @@ -1318,12 +1402,15 @@ impl BeginScanError { pub mod local_test_utils { use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::{ - AccessibleScanner, BeginScanError, InaccessibleScanner, MultistageDualPayableScanner, - PreparedAdjustment, PrivateScanStarter, ScanWithStarter, - SolvencySensitivePaymentInstructor, StartableAccessibleScanner, UiScanResult, + BeginScanError, MultistageDualPayableScanner, PreparedAdjustment, PrivateScanner, + PrivateScannerAccessToken, PrivateScannerWithAccessToken, + PrivateStartableScannerAccessToken, PrivateStartableScannerWithAccessToken, Scanner, + SolvencySensitivePaymentInstructor, StartableScanner, UiScanResult, }; - use crate::accountant::BlockchainAgentWithContextMessage; use crate::accountant::OutboundPaymentsInstructions; + use crate::accountant::{ + BlockchainAgentWithContextMessage, ScanForNewPayables, ScanForRetryPayables, + }; use crate::accountant::{ResponseSkeleton, SentPayables}; use crate::sub_lib::wallet::Wallet; use actix::{Message, System}; @@ -1336,7 +1423,7 @@ pub mod local_test_utils { pub struct NullScanner {} impl - StartableAccessibleScanner for NullScanner + PrivateScanner for NullScanner where TriggerMessage: Message, StartMessage: Message, @@ -1344,19 +1431,30 @@ pub mod local_test_utils { { } - impl ScanWithStarter for NullScanner + impl + PrivateStartableScannerWithAccessToken for NullScanner where TriggerMessage: Message, StartMessage: Message, { - fn scan_starter(&mut self) -> PrivateScanStarter { + fn access_startable_scanner( + &mut self, + ) -> PrivateStartableScannerAccessToken { unimplemented!("Not needed yet") } } - impl AccessibleScanner for NullScanner + impl PrivateScannerWithAccessToken for NullScanner + where + EndMessage: Message, + { + fn access_scanner(&mut self) -> PrivateScannerAccessToken { + unimplemented!("Not needed yet") + } + } + + impl Scanner for NullScanner where - StartMessage: Message, EndMessage: Message, { fn finish_scan(&mut self, _message: EndMessage, _logger: &Logger) -> UiScanResult { @@ -1378,7 +1476,7 @@ pub mod local_test_utils { as_any_ref_in_trait_impl!(); } - impl MultistageDualPayableScanner for NullScanner {} + impl MultistageDualPayableScanner for NullScanner {} impl SolvencySensitivePaymentInstructor for NullScanner { fn try_skipping_payment_adjustment( @@ -1398,7 +1496,7 @@ pub mod local_test_utils { } } - impl InaccessibleScanner for NullScanner + impl StartableScanner for NullScanner where TriggerMessage: Message, StartMessage: Message, @@ -1435,29 +1533,18 @@ pub mod local_test_utils { stop_system_after_last_message: RefCell, } - impl - StartableAccessibleScanner - for ScannerMock - where - TriggerMessage: Message, - StartMessage: Message, - EndMessage: Message, - { - } - - impl ScanWithStarter + impl PrivateScannerWithAccessToken for ScannerMock where - TriggerMessage: Message, StartMessage: Message, EndMessage: Message, { - fn scan_starter(&mut self) -> PrivateScanStarter { - PrivateScanStarter::new(self) + fn access_scanner(&mut self) -> PrivateScannerAccessToken { + PrivateScannerAccessToken::new(self) } } - impl InaccessibleScanner + impl StartableScanner for ScannerMock where TriggerMessage: Message, @@ -1484,8 +1571,7 @@ pub mod local_test_utils { } } - impl AccessibleScanner - for ScannerMock + impl Scanner for ScannerMock where StartMessage: Message, EndMessage: Message, @@ -1587,11 +1673,30 @@ pub mod local_test_utils { } } - impl MultistageDualPayableScanner + impl PrivateStartableScannerWithAccessToken for ScannerMock { + fn access_startable_scanner( + &mut self, + ) -> PrivateStartableScannerAccessToken + { + todo!() + } } + impl PrivateStartableScannerWithAccessToken + for ScannerMock + { + fn access_startable_scanner( + &mut self, + ) -> PrivateStartableScannerAccessToken + { + todo!() + } + } + + impl MultistageDualPayableScanner for ScannerMock {} + impl SolvencySensitivePaymentInstructor for ScannerMock { fn try_skipping_payment_adjustment( &self, @@ -1629,7 +1734,7 @@ mod tests { use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PendingPayableMetadata; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; - use crate::accountant::scanners::{AccessibleScanner, BeginScanError, InaccessibleScanner, PayableScanner, PendingPayableScanner, PrivateScanStarter, ReceivableScanner, ScanWithStarter, ScannerCommon, Scanners}; + use crate::accountant::scanners::{Scanner, BeginScanError, StartableScanner, PayableScanner, PendingPayableScanner, PrivateScannerAccessToken, ReceivableScanner, PrivateScannerWithAccessToken, ScannerCommon, Scanners}; use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanForNewPayables, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; @@ -1666,9 +1771,65 @@ mod tests { use web3::types::{TransactionReceipt, H256}; use web3::Error; use masq_lib::messages::ScanType; - use crate::accountant::scanners::local_test_utils::NullScanner; + use crate::accountant::scanners::local_test_utils::{NullScanner, ScannerMock}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; + enum ScannerReplacement { + Payable(ScannerMock), + PendingPayable(ScannerMock), + Receivable(ScannerMock), + } + + impl Scanners { + pub fn replace_scanner(&mut self, replacement: ScannerReplacement) { + match replacement { + ScannerReplacement::Payable(scanner) => self.payable = Box::new(scanner), + ScannerReplacement::PendingPayable(scanner) => { + self.pending_payable = Box::new(scanner) + } + ScannerReplacement::Receivable(scanner) => self.receivable = Box::new(scanner), + } + } + + pub fn reset_scan_started( + &mut self, + scan_type: ScanType, + value_opt: Option, + logger: &Logger, + ) { + match scan_type { + ScanType::Payables => Self::scanner_timestamp_treatment( + self.payable.access_scanner().scanner, + value_opt, + logger, + ), + ScanType::PendingPayables => Self::scanner_timestamp_treatment( + self.pending_payable.access_scanner().scanner, + value_opt, + logger, + ), + ScanType::Receivables => Self::scanner_timestamp_treatment( + self.receivable.access_scanner().scanner, + value_opt, + logger, + ), + } + } + + fn scanner_timestamp_treatment( + scanner: &mut dyn Scanner, + value_opt: Option, + logger: &Logger, + ) where + Message: actix::Message, + { + match value_opt { + None => scanner.mark_as_ended(logger), + Some(timestamp) => scanner.mark_as_started(timestamp), + } + } + } + #[test] fn scanners_struct_can_be_constructed_with_the_respective_scanners() { let payable_dao_factory = PayableDaoFactoryMock::new() @@ -1788,7 +1949,7 @@ mod tests { now, None, &Logger::new(test_name), - false + false, ); let timestamp = subject.payable.scan_started_at(); @@ -1830,7 +1991,7 @@ mod tests { previous_scan_started_at, None, &Logger::new("test"), - false + false, ); let result = subject.start_new_payable_scan_guarded( @@ -1838,7 +1999,7 @@ mod tests { SystemTime::now(), None, &Logger::new("test"), - false + false, ); let is_scan_running = subject.payable.scan_started_at().is_some(); @@ -1863,7 +2024,7 @@ mod tests { let mut subject = PayableScannerBuilder::new() .payable_dao(payable_dao) .build(); - let starter: PrivateScanStarter = subject.scan_starter(); + let starter: PrivateScannerAccessToken = subject.access_scanner(); let result = starter .scanner @@ -2009,7 +2170,7 @@ mod tests { let mut subject = PayableScannerBuilder::new() .payable_dao(payable_dao) .build(); - let starter: PrivateScanStarter = subject.scan_starter(); + let starter: PrivateScannerAccessToken = subject.access_scanner(); let _ = starter .scanner @@ -2927,7 +3088,7 @@ mod tests { now, None, &Logger::new(test_name), - false + false, ); let no_of_pending_payables = fingerprints.len(); @@ -2962,14 +3123,20 @@ mod tests { let payable_scanner = PayableScannerBuilder::new().build(); subject.payable = Box::new(payable_scanner); let logger = Logger::new("test"); - let _ = subject.start_pending_payable_scan_guarded(&consuming_wallet, now, None, &logger, false); + let _ = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + now, + None, + &logger, + false, + ); let result = subject.start_pending_payable_scan_guarded( &consuming_wallet, SystemTime::now(), None, &logger, - false + false, ); let is_scan_running = subject.pending_payable.scan_started_at().is_some(); @@ -3000,7 +3167,7 @@ mod tests { SystemTime::now(), None, &logger, - false + false, ); let is_scan_running = subject.pending_payable.scan_started_at().is_some(); @@ -3039,7 +3206,7 @@ mod tests { SystemTime::now(), None, &Logger::new("test"), - false + false, ); })) .unwrap_err(); @@ -4128,8 +4295,8 @@ mod tests { )); } - fn assert_elapsed_time_in_mark_as_ended( - subject: &mut dyn AccessibleScanner, + fn assert_elapsed_time_in_mark_as_ended( + subject: &mut dyn Scanner, scanner_name: &str, test_name: &str, logger: &Logger, diff --git a/node/src/accountant/scanners/payable_scanner_extension/mod.rs b/node/src/accountant/scanners/payable_scanner_extension/mod.rs index 54a738f16..2503d568a 100644 --- a/node/src/accountant/scanners/payable_scanner_extension/mod.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/mod.rs @@ -7,22 +7,23 @@ pub mod msgs; pub mod test_utils; use crate::accountant::payment_adjuster::Adjustment; -use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::{AccessibleScanner, ScanWithStarter}; -use crate::accountant::{ScanForNewPayables, ScanForRetryPayables}; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + BlockchainAgentWithContextMessage, QualifiedPayablesMessage, +}; +use crate::accountant::scanners::{ + PrivateScannerWithAccessToken, PrivateStartableScannerWithAccessToken, Scanner, +}; +use crate::accountant::{ScanForNewPayables, ScanForRetryPayables, SentPayables}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use actix::Message; use itertools::Either; use masq_lib::logger::Logger; -pub trait MultistageDualPayableScanner: - ScanWithStarter - + ScanWithStarter - + AccessibleScanner +pub trait MultistageDualPayableScanner: + PrivateStartableScannerWithAccessToken + + PrivateStartableScannerWithAccessToken + + PrivateScannerWithAccessToken + SolvencySensitivePaymentInstructor -where - StartMessage: Message, - EndMessage: Message, { } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 74ff7b4c5..464f81578 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -20,7 +20,7 @@ pub struct ScanSchedulers { pub pending_payable: SimplePeriodicalScanScheduler, pub receivable: SimplePeriodicalScanScheduler, pub pending_payable_sequence_in_process: bool, - pub automatic_scans_enabled: bool + pub automatic_scans_enabled: bool, } impl ScanSchedulers { @@ -30,9 +30,7 @@ impl ScanSchedulers { pending_payable: SimplePeriodicalScanScheduler::new( scan_intervals.pending_payable_scan_interval, ), - receivable: SimplePeriodicalScanScheduler::new( - scan_intervals.receivable_scan_interval, - ), + receivable: SimplePeriodicalScanScheduler::new(scan_intervals.receivable_scan_interval), pending_payable_sequence_in_process: false, automatic_scans_enabled, } @@ -308,11 +306,7 @@ mod tests { .borrow(), false ); - assert_eq!( - schedulers - .pending_payable_sequence_in_process, - false - ); + assert_eq!(schedulers.pending_payable_sequence_in_process, false); assert_eq!(schedulers.automatic_scans_enabled, true) } diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 9a56130d9..8f1cc48f6 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -312,8 +312,8 @@ pub mod pending_payable_scanner_utils { use crate::accountant::PendingPayableId; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use masq_lib::logger::Logger; - use std::time::SystemTime; use masq_lib::ui_gateway::NodeToUiMessage; + use std::time::SystemTime; #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct PendingPayableScanReport { @@ -327,10 +327,10 @@ pub mod pending_payable_scanner_utils { !self.still_pending.is_empty() || !self.failures.is_empty() } } - + pub enum PendingPayableScanResult { PendingPayablesFinished(Option), - PaymentRetryRequired + PaymentRetryRequired, } pub fn elapsed_in_ms(timestamp: SystemTime) -> u128 { From 231ae8ae9317d14f442e32e00eaa7f97540d5748 Mon Sep 17 00:00:00 2001 From: Bert Date: Fri, 9 May 2025 15:11:04 +0200 Subject: [PATCH 14/49] GH-602: finally fully scanners as flexible parametrized generic objects --- node/src/accountant/mod.rs | 138 +++-- node/src/accountant/scanners/mod.rs | 552 +++++++----------- .../scanners/payable_scanner_extension/mod.rs | 15 +- .../src/accountant/scanners/scanners_utils.rs | 3 +- node/src/accountant/scanners/test_utils.rs | 51 ++ 5 files changed, 370 insertions(+), 389 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index ef6575a62..0178b9fef 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -25,7 +25,7 @@ use crate::accountant::financials::visibility_restricted_module::{ use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; -use crate::accountant::scanners::{BeginScanError, Scanners, UiScanResult}; +use crate::accountant::scanners::{BeginScanError, Scanners}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, PendingPayableFingerprintSeeds, RetrieveTransactions}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; @@ -256,7 +256,7 @@ impl Handler for Accountant { fn handle(&mut self, msg: ReportTransactionReceipts, ctx: &mut Self::Context) -> Self::Result { let response_skeleton_opt = msg.response_skeleton_opt; match self.scanners.finish_pending_payable_scan(msg, &self.logger) { - PendingPayableScanResult::PendingPayablesFinished(ui_msg_opt) => { + PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) => { if let Some(node_to_ui_msg) = ui_msg_opt { self.ui_message_sub_opt .as_ref() @@ -730,15 +730,13 @@ impl Accountant { fn handle_payable_payment_setup(&mut self, msg: BlockchainAgentWithContextMessage) { let blockchain_bridge_instructions = match self .scanners - .payable - .try_skipping_payment_adjustment(msg, &self.logger) + .try_skipping_payable_adjustment(msg, &self.logger) { Ok(Either::Left(finalized_msg)) => finalized_msg, Ok(Either::Right(unaccepted_msg)) => { //TODO we will eventually query info from Neighborhood before the adjustment, according to GH-699 self.scanners - .payable - .perform_payment_adjustment(unaccepted_msg, &self.logger) + .perform_payable_adjustment(unaccepted_msg, &self.logger) } Err(_e) => todo!("be completed by GH-711"), }; @@ -1151,8 +1149,8 @@ mod tests { use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::{NewPayableScanDynIntervalComputerMock}; - use crate::accountant::scanners::{BeginScanError}; + use crate::accountant::scanners::test_utils::{MarkScanner, NewPayableScanDynIntervalComputerMock, ReplacementType, ScannerReplacement}; + use crate::accountant::scanners::{BeginScanError, ReceivableScanner}; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; @@ -1675,7 +1673,11 @@ mod tests { let payable_scanner = PayableScannerBuilder::new() .payment_adjuster(payment_adjuster) .build(); - subject.scanners.payable = Box::new(payable_scanner); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Real( + payable_scanner, + ))); subject.outbound_payments_instructions_sub_opt = Some(instructions_recipient); subject.logger = Logger::new(test_name); let subject_addr = subject.start(); @@ -1804,7 +1806,11 @@ mod tests { let payable_scanner = PayableScannerBuilder::new() .payment_adjuster(payment_adjuster) .build(); - subject.scanners.payable = Box::new(payable_scanner); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Real( + payable_scanner, + ))); subject.outbound_payments_instructions_sub_opt = Some(report_recipient); subject.logger = Logger::new(test_name); let subject_addr = subject.start(); @@ -2041,8 +2047,12 @@ mod tests { .consuming_wallet(consuming_wallet.clone()) .payable_daos(vec![ForPayableScanner(payable_dao)]) .build(); - subject.scanners.pending_payable = Box::new(NullScanner::new()); - subject.scanners.receivable = Box::new(NullScanner::new()); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); let peer_actors = peer_actors_builder() @@ -2092,9 +2102,17 @@ mod tests { .started_at_result(None) .start_scan_params(&start_scan_params_arc) .start_scan_result(Ok(qualified_payables_msg.clone())); - subject.scanners.payable = Box::new(payable_scanner_mock); - subject.scanners.pending_payable = Box::new(NullScanner::new()); - subject.scanners.receivable = Box::new(NullScanner::new()); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner_mock, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); let response_skeleton_opt = Some(ResponseSkeleton { client_id: 789, context_id: 111, @@ -2148,8 +2166,12 @@ mod tests { .bootstrapper_config(bc_from_earning_wallet(earning_wallet.clone())) .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) .build(); - subject.scanners.pending_payable = Box::new(NullScanner::new()); - subject.scanners.payable = Box::new(NullScanner::new()); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Null)); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); let peer_actors = peer_actors_builder() @@ -2374,9 +2396,17 @@ mod tests { .bootstrapper_config(config) .logger(Logger::new(test_name)) .build(); - subject.scanners.payable = Box::new(NullScanner::new()); // Skipping - subject.scanners.pending_payable = Box::new(NullScanner::new()); // Skipping - subject.scanners.receivable = Box::new(receivable_scanner); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Null)); // Skipping + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); // Skipping + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Mock( + receivable_scanner, + ))); subject.scan_schedulers.receivable.handle = Box::new( NotifyLaterHandleMock::default() .notify_later_params(¬ify_later_receivable_params_arc) @@ -2504,7 +2534,7 @@ mod tests { .started_at_result(None) .start_scan_params(&start_scan_pending_payable_params_arc) .start_scan_result(Ok(request_transaction_receipts.clone())) - .finish_scan_result(UiScanResult::Finished(None)); + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); let qualified_payables_msg = QualifiedPayablesMessage { qualified_payables: qualified_payable.clone(), consuming_wallet: consuming_wallet.clone(), @@ -2516,7 +2546,7 @@ mod tests { .started_at_result(None) .start_scan_params(&start_scan_payable_params_arc) .start_scan_result(Ok(qualified_payables_msg.clone())) - .finish_scan_result(UiScanResult::Finished(None)); + .finish_scan_result(None); let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { // This simply means that we're gonna surplus this value (it abides by how many pending @@ -2530,9 +2560,19 @@ mod tests { .consuming_wallet(consuming_wallet.clone()) .logger(Logger::new(test_name)) .build(); - subject.scanners.pending_payable = Box::new(pending_payable_scanner); - subject.scanners.payable = Box::new(payable_scanner); - subject.scanners.receivable = Box::new(NullScanner::new()); //skipping + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); //skipping subject.scan_schedulers.pending_payable.handle = Box::new( NotifyLaterHandleMock::::default() .notify_later_params(¬ify_later_pending_payables_params_arc) @@ -2647,7 +2687,10 @@ mod tests { subject.handle_request_of_scan_for_new_payable(None); - let has_scan_started = subject.scanners.payable.scan_started_at().is_some(); + let has_scan_started = subject + .scanners + .scan_started_at(ScanType::Payables) + .is_some(); assert_eq!(has_scan_started, false); TestLogHandler::new().exists_log_containing(&format!( "DEBUG: {test_name}: Cannot initiate Payables scan because no consuming wallet was found." @@ -2664,7 +2707,10 @@ mod tests { subject.handle_request_of_scan_for_pending_payable(None); - let has_scan_started = subject.scanners.pending_payable.scan_started_at().is_some(); + let has_scan_started = subject + .scanners + .scan_started_at(ScanType::PendingPayables) + .is_some(); assert_eq!(has_scan_started, false); TestLogHandler::new().exists_log_containing(&format!( "DEBUG: {test_name}: Cannot initiate PendingPayables scan because no consuming wallet was found." @@ -2831,8 +2877,12 @@ mod tests { .consuming_wallet(consuming_wallet.clone()) .payable_daos(vec![ForPayableScanner(payable_dao)]) .build(); - subject.scanners.pending_payable = Box::new(NullScanner::new()); - subject.scanners.receivable = Box::new(NullScanner::new()); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); let response_skeleton_opt = Some(ResponseSkeleton { client_id: 31, @@ -3699,8 +3749,12 @@ mod tests { .build(); let pending_payable_scanner = ScannerMock::new() .finish_scan_params(&finish_scan_params_arc) - .finish_scan_result(UiScanResult::ChainedScannersUnfinished); - subject.scanners.pending_payable = Box::new(pending_payable_scanner); + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); subject.scan_schedulers.payable.retry_payable_notify = Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); let system = System::new(test_name); @@ -4907,17 +4961,9 @@ mod tests { let mut subject = AccountantBuilder::default() .logger(Logger::new(test_name)) .build(); - match message.scan_type { - ScanType::Payables => subject.scanners.payable.mark_as_started(SystemTime::now()), - ScanType::PendingPayables => subject - .scanners - .pending_payable - .mark_as_started(SystemTime::now()), - ScanType::Receivables => subject - .scanners - .receivable - .mark_as_started(SystemTime::now()), - } + subject + .scanners + .reset_scan_started(message.scan_type, MarkScanner::Started(SystemTime::now())); let subject_addr = subject.start(); let system = System::new("test"); let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); @@ -4928,13 +4974,7 @@ mod tests { subject_addr .try_send(AssertionsMessage { assertions: Box::new(move |actor: &mut Accountant| { - let scan_started_at_opt = match message.scan_type { - ScanType::Payables => actor.scanners.payable.scan_started_at(), - ScanType::PendingPayables => { - actor.scanners.pending_payable.scan_started_at() - } - ScanType::Receivables => actor.scanners.receivable.scan_started_at(), - }; + let scan_started_at_opt = actor.scanners.scan_started_at(message.scan_type); assert_eq!(scan_started_at_opt, None); }), }) diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 52e543ac1..de296710c 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -57,7 +57,7 @@ use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_le use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::db_config::persistent_configuration::{PersistentConfiguration, PersistentConfigurationReal}; -// Keep the individual scanner objects private! +// Leave the individual scanner objects private! pub struct Scanners { payable: Box, pending_payable: Box< @@ -65,9 +65,17 @@ pub struct Scanners { ScanForPendingPayables, RequestTransactionReceipts, ReportTransactionReceipts, + PendingPayableScanResult, + >, + >, + receivable: Box< + dyn PrivateScanner< + ScanForReceivables, + RetrieveTransactions, + ReceivedPayments, + Option, >, >, - receivable: Box>, } impl Scanners { @@ -127,23 +135,20 @@ impl Scanners { todo!() // ManualTriggerError } - let scanner = self.payable.access_scanner().scanner; - - if let Some(started_at) = scanner.scan_started_at() { + if let Some(started_at) = self.payable.scan_started_at() { return Err(BeginScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Payables, started_at, }); } - let starter: PrivateStartableScannerAccessToken< - ScanForNewPayables, - QualifiedPayablesMessage, - > = self.payable.access_startable_scanner(); - - starter - .scanner - .start_scan(wallet, timestamp, response_skeleton_opt, logger) + Self::start_correct_payable_scanner::( + &mut *self.payable, + wallet, + timestamp, + response_skeleton_opt, + logger, + ) } // Note: This scanner cannot be started on its own, it always comes after the pending payable @@ -155,8 +160,7 @@ impl Scanners { response_skeleton_opt: Option, logger: &Logger, ) -> Result { - let scanner = self.payable.access_scanner().scanner; - if let Some(started_at) = scanner.scan_started_at() { + if let Some(started_at) = self.payable.scan_started_at() { unreachable!( "Guards should ensure that no payable scanner can run if the pending payable \ repetitive sequence is still ongoing. However, some other payable scan intruded \ @@ -166,14 +170,13 @@ impl Scanners { ) } - let starter: PrivateStartableScannerAccessToken< - ScanForRetryPayables, - QualifiedPayablesMessage, - > = self.payable.access_startable_scanner(); - - starter - .scanner - .start_scan(wallet, timestamp, response_skeleton_opt, logger) + Self::start_correct_payable_scanner::( + &mut *self.payable, + wallet, + timestamp, + response_skeleton_opt, + logger, + ) } pub fn start_pending_payable_scan_guarded( @@ -189,11 +192,9 @@ impl Scanners { todo!() // ManualTriggerError } - let pending_payable_scanner = self.pending_payable.access_scanner().scanner; - let payable_scanner = self.payable.access_scanner().scanner; match ( - pending_payable_scanner.scan_started_at(), - payable_scanner.scan_started_at(), + self.pending_payable.scan_started_at(), + self.payable.scan_started_at(), ) { (Some(pp_timestamp), Some(p_timestamp)) => // If you're wondering, then yes, this should be sacre in the relationship between @@ -221,8 +222,6 @@ impl Scanners { (None, None) => (), } self.pending_payable - .access_startable_scanner() - .scanner .start_scan(wallet, timestamp, response_skeleton_opt, logger) } @@ -233,16 +232,13 @@ impl Scanners { response_skeleton_opt: Option, logger: &Logger, ) -> Result { - let scanner = self.receivable.access_scanner().scanner; - if let Some(started_at) = scanner.scan_started_at() { + if let Some(started_at) = self.receivable.scan_started_at() { return Err(BeginScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Receivables, started_at, }); } self.receivable - .access_startable_scanner() - .scanner .start_scan(wallet, timestamp, response_skeleton_opt, logger) } @@ -276,7 +272,7 @@ impl Scanners { pub fn try_skipping_payable_adjustment( &self, - msg: &BlockchainAgentWithContextMessage, + msg: BlockchainAgentWithContextMessage, logger: &Logger, ) -> Result, String> { todo!() @@ -289,11 +285,28 @@ impl Scanners { ) -> OutboundPaymentsInstructions { todo!() } + + fn start_correct_payable_scanner<'a, TriggerMessage>( + scanner: &'a mut (dyn MultistageDualPayableScanner + 'a), + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result + where + TriggerMessage: Message, + (dyn MultistageDualPayableScanner + 'a): + StartableScanner, + { + <(dyn MultistageDualPayableScanner + 'a) as StartableScanner< + TriggerMessage, + QualifiedPayablesMessage, + >>::start_scan(scanner, wallet, timestamp, response_skeleton_opt, logger) + } } -trait PrivateScanner: - PrivateStartableScannerWithAccessToken - + PrivateScannerWithAccessToken +trait PrivateScanner: + StartableScanner + Scanner where TriggerMessage: Message, StartMessage: Message, @@ -301,23 +314,27 @@ where { } -pub trait PrivateStartableScannerWithAccessToken { - fn access_startable_scanner( +trait StartableScanner +where + TriggerMessage: Message, + StartMessage: Message, +{ + fn start_scan( &mut self, - ) -> PrivateStartableScannerAccessToken; -} - -pub trait PrivateScannerWithAccessToken { - fn access_scanner(&mut self) -> PrivateScannerAccessToken; + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result; } -trait Scanner +trait Scanner where EndMessage: Message, { //TODO this is an old initiative, should go away // fn access_scanner(starter: &mut dyn StartableScanner) -> PrivateScannerAccessToken where Self: Sized; - fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> UiScanResult; + fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> ScanResult; fn scan_started_at(&self) -> Option; fn mark_as_started(&mut self, timestamp: SystemTime); fn mark_as_ended(&mut self, logger: &Logger); @@ -326,66 +343,6 @@ where as_any_mut_in_trait!(); } -//TODO replace me with a generic possibly different for each scanner -#[derive(Debug, PartialEq)] -pub enum UiScanResult { - Finished(Option), - ChainedScannersUnfinished, -} - -impl UiScanResult { - pub fn finished(self) -> Option { - if let UiScanResult::Finished(ui_msg_opt) = self { - ui_msg_opt - } else { - todo!("test drive a panic...") - } - } -} - -pub struct PrivateStartableScannerAccessToken<'s, TriggerMessage, StartMessage> { - // Strictly private - scanner: &'s mut dyn StartableScanner, -} - -impl<'s, TriggerMessage, StartMessage> - PrivateStartableScannerAccessToken<'s, TriggerMessage, StartMessage> -{ - fn new( - scanner: &'s mut dyn StartableScanner, - ) -> PrivateStartableScannerAccessToken<'s, TriggerMessage, StartMessage> { - Self { scanner } - } -} - -// Using this access token to screen away the private interface from its public counterpart, because -// Rust doesn't allow selective private methods within a public trait -pub struct PrivateScannerAccessToken<'s, EndMessage> { - // Strictly private - scanner: &'s mut dyn Scanner, -} - -impl<'s, EndMessage> PrivateScannerAccessToken<'s, EndMessage> { - fn new(scanner: &'s mut dyn Scanner) -> PrivateScannerAccessToken<'s, EndMessage> { - Self { scanner } - } -} - -// Strictly private -trait StartableScanner -where - TriggerMessage: Message, - StartMessage: Message, -{ - fn start_scan( - &mut self, - wallet: &Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, - ) -> Result; -} - pub struct ScannerCommon { initiated_at_opt: Option, payment_thresholds: Rc, @@ -509,36 +466,8 @@ impl StartableScanner for Payabl } } -impl PrivateStartableScannerWithAccessToken - for PayableScanner -{ - fn access_startable_scanner( - &mut self, - ) -> PrivateStartableScannerAccessToken { - todo!(); - PrivateStartableScannerAccessToken::new(self) - } -} - -impl PrivateStartableScannerWithAccessToken - for PayableScanner -{ - fn access_startable_scanner( - &mut self, - ) -> PrivateStartableScannerAccessToken { - PrivateStartableScannerAccessToken::new(self) - } -} - -impl PrivateScannerWithAccessToken for PayableScanner { - fn access_scanner(&mut self) -> PrivateScannerAccessToken { - todo!(); - PrivateScannerAccessToken::new(self) - } -} - -impl Scanner for PayableScanner { - fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> UiScanResult { +impl Scanner> for PayableScanner { + fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> Option { let (sent_payables, err_opt) = separate_errors(&message, logger); debug!( logger, @@ -553,12 +482,12 @@ impl Scanner for PayableScanner { self.mark_as_ended(logger); - UiScanResult::Finished(message.response_skeleton_opt.map(|response_skeleton| { - NodeToUiMessage { + message + .response_skeleton_opt + .map(|response_skeleton| NodeToUiMessage { target: MessageTarget::ClientId(response_skeleton.client_id), body: UiScanResponse {}.tmb(response_skeleton.context_id), - } - })) + }) } time_marking_methods!(Payables); @@ -856,28 +785,14 @@ pub struct PendingPayableScanner { pub financial_statistics: Rc>, } -impl PrivateScanner - for PendingPayableScanner -{ -} - -impl PrivateStartableScannerWithAccessToken - for PendingPayableScanner +impl + PrivateScanner< + ScanForPendingPayables, + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + > for PendingPayableScanner { - fn access_startable_scanner( - &mut self, - ) -> PrivateStartableScannerAccessToken - { - todo!(); - PrivateStartableScannerAccessToken::new(self) - } -} - -impl PrivateScannerWithAccessToken for PendingPayableScanner { - fn access_scanner(&mut self) -> PrivateScannerAccessToken { - todo!(); - PrivateScannerAccessToken::new(self) - } } impl StartableScanner @@ -913,8 +828,12 @@ impl StartableScanner } } -impl Scanner for PendingPayableScanner { - fn finish_scan(&mut self, message: ReportTransactionReceipts, logger: &Logger) -> UiScanResult { +impl Scanner for PendingPayableScanner { + fn finish_scan( + &mut self, + message: ReportTransactionReceipts, + logger: &Logger, + ) -> PendingPayableScanResult { let construct_msg_scan_ended_to_ui = move || { message .response_skeleton_opt @@ -947,7 +866,7 @@ impl Scanner for PendingPayableScanner { if requires_payments_retry { todo!() } else { - UiScanResult::Finished(construct_msg_scan_ended_to_ui()) + PendingPayableScanResult::NoPendingPayablesLeft(construct_msg_scan_ended_to_ui()) } } } @@ -1124,29 +1043,16 @@ pub struct ReceivableScanner { pub financial_statistics: Rc>, } -impl PrivateScanner - for ReceivableScanner +impl + PrivateScanner< + ScanForReceivables, + RetrieveTransactions, + ReceivedPayments, + Option, + > for ReceivableScanner { } -impl PrivateStartableScannerWithAccessToken - for ReceivableScanner -{ - fn access_startable_scanner( - &mut self, - ) -> PrivateStartableScannerAccessToken { - todo!(); - PrivateStartableScannerAccessToken::new(self) - } -} - -impl PrivateScannerWithAccessToken for ReceivableScanner { - fn access_scanner(&mut self) -> PrivateScannerAccessToken { - todo!(); - PrivateScannerAccessToken::new(self) - } -} - impl StartableScanner for ReceivableScanner { fn start_scan( &mut self, @@ -1166,7 +1072,7 @@ impl StartableScanner for ReceivableSc } } -impl Scanner for ReceivableScanner { +impl Scanner> for ReceivableScanner { // fn start_scan( // &mut self, // earning_wallet: Wallet, @@ -1187,17 +1093,15 @@ impl Scanner for ReceivableScanner { // }) // } - fn finish_scan(&mut self, msg: ReceivedPayments, logger: &Logger) -> UiScanResult { + fn finish_scan(&mut self, msg: ReceivedPayments, logger: &Logger) -> Option { self.handle_new_received_payments(&msg, logger); self.mark_as_ended(logger); - UiScanResult::Finished( - msg.response_skeleton_opt - .map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }), - ) + msg.response_skeleton_opt + .map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }) } time_marking_methods!(Receivables); @@ -1402,10 +1306,8 @@ impl BeginScanError { pub mod local_test_utils { use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::{ - BeginScanError, MultistageDualPayableScanner, PreparedAdjustment, PrivateScanner, - PrivateScannerAccessToken, PrivateScannerWithAccessToken, - PrivateStartableScannerAccessToken, PrivateStartableScannerWithAccessToken, Scanner, - SolvencySensitivePaymentInstructor, StartableScanner, UiScanResult, + BeginScanError, MultistageDualPayableScanner, PreparedAdjustment, PrivateScanner, Scanner, + SolvencySensitivePaymentInstructor, StartableScanner, }; use crate::accountant::OutboundPaymentsInstructions; use crate::accountant::{ @@ -1416,14 +1318,16 @@ pub mod local_test_utils { use actix::{Message, System}; use itertools::Either; use masq_lib::logger::Logger; + use masq_lib::ui_gateway::NodeToUiMessage; + use rand::distributions::Standard; use std::cell::RefCell; use std::sync::{Arc, Mutex}; use std::time::SystemTime; pub struct NullScanner {} - impl - PrivateScanner for NullScanner + impl + PrivateScanner for NullScanner where TriggerMessage: Message, StartMessage: Message, @@ -1431,33 +1335,27 @@ pub mod local_test_utils { { } - impl - PrivateStartableScannerWithAccessToken for NullScanner + impl StartableScanner for NullScanner where TriggerMessage: Message, StartMessage: Message, { - fn access_startable_scanner( + fn start_scan( &mut self, - ) -> PrivateStartableScannerAccessToken { - unimplemented!("Not needed yet") - } - } - - impl PrivateScannerWithAccessToken for NullScanner - where - EndMessage: Message, - { - fn access_scanner(&mut self) -> PrivateScannerAccessToken { - unimplemented!("Not needed yet") + _wallet: &Wallet, + _timestamp: SystemTime, + _response_skeleton_opt: Option, + _logger: &Logger, + ) -> Result { + Err(BeginScanError::CalledFromNullScanner) } } - impl Scanner for NullScanner + impl Scanner for NullScanner where EndMessage: Message, { - fn finish_scan(&mut self, _message: EndMessage, _logger: &Logger) -> UiScanResult { + fn finish_scan(&mut self, _message: EndMessage, _logger: &Logger) -> ScanResult { panic!("Called finish_scan() from NullScanner"); } @@ -1496,22 +1394,6 @@ pub mod local_test_utils { } } - impl StartableScanner for NullScanner - where - TriggerMessage: Message, - StartMessage: Message, - { - fn start_scan( - &mut self, - _wallet: &Wallet, - _timestamp: SystemTime, - _response_skeleton_opt: Option, - _logger: &Logger, - ) -> Result { - Err(BeginScanError::CalledFromNullScanner) - } - } - impl Default for NullScanner { fn default() -> Self { Self::new() @@ -1524,28 +1406,28 @@ pub mod local_test_utils { } } - pub struct ScannerMock { + pub struct ScannerMock { start_scan_params: Arc, Logger)>>>, start_scan_results: RefCell>>, finish_scan_params: Arc>>, - finish_scan_results: RefCell>, + finish_scan_results: RefCell>, started_at_results: RefCell>>, stop_system_after_last_message: RefCell, } - impl PrivateScannerWithAccessToken - for ScannerMock + impl + PrivateScanner + for ScannerMock where + TriggerMessage: Message, StartMessage: Message, EndMessage: Message, { - fn access_scanner(&mut self) -> PrivateScannerAccessToken { - PrivateScannerAccessToken::new(self) - } } - impl StartableScanner - for ScannerMock + impl + StartableScanner + for ScannerMock where TriggerMessage: Message, StartMessage: Message, @@ -1571,12 +1453,13 @@ pub mod local_test_utils { } } - impl Scanner for ScannerMock + impl Scanner + for ScannerMock where StartMessage: Message, EndMessage: Message, { - fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> UiScanResult { + fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> ScanResult { self.finish_scan_params .lock() .unwrap() @@ -1600,13 +1483,15 @@ pub mod local_test_utils { } } - impl Default for ScannerMock { + impl Default + for ScannerMock + { fn default() -> Self { Self::new() } } - impl ScannerMock { + impl ScannerMock { pub fn new() -> Self { Self { start_scan_params: Arc::new(Mutex::new(vec![])), @@ -1644,7 +1529,7 @@ pub mod local_test_utils { self } - pub fn finish_scan_result(self, result: UiScanResult) -> Self { + pub fn finish_scan_result(self, result: ScanResult) -> Self { self.finish_scan_results.borrow_mut().push(result); self } @@ -1673,31 +1558,14 @@ pub mod local_test_utils { } } - impl PrivateStartableScannerWithAccessToken - for ScannerMock + impl MultistageDualPayableScanner + for ScannerMock> { - fn access_startable_scanner( - &mut self, - ) -> PrivateStartableScannerAccessToken - { - todo!() - } } - impl PrivateStartableScannerWithAccessToken - for ScannerMock + impl SolvencySensitivePaymentInstructor + for ScannerMock> { - fn access_startable_scanner( - &mut self, - ) -> PrivateStartableScannerAccessToken - { - todo!() - } - } - - impl MultistageDualPayableScanner for ScannerMock {} - - impl SolvencySensitivePaymentInstructor for ScannerMock { fn try_skipping_payment_adjustment( &self, msg: BlockchainAgentWithContextMessage, @@ -1733,8 +1601,8 @@ mod tests { use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t}; use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PendingPayableMetadata; - use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport}; - use crate::accountant::scanners::{Scanner, BeginScanError, StartableScanner, PayableScanner, PendingPayableScanner, PrivateScannerAccessToken, ReceivableScanner, PrivateScannerWithAccessToken, ScannerCommon, Scanners}; + use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport, PendingPayableScanResult}; + use crate::accountant::scanners::{Scanner, BeginScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, PrivateScanner}; use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanForNewPayables, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; @@ -1768,64 +1636,80 @@ mod tests { use std::rc::Rc; use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration, SystemTime}; + use itertools::Either; use web3::types::{TransactionReceipt, H256}; use web3::Error; use masq_lib::messages::ScanType; + use masq_lib::ui_gateway::NodeToUiMessage; use crate::accountant::scanners::local_test_utils::{NullScanner, ScannerMock}; + use crate::accountant::scanners::test_utils::{MarkScanner, ReplacementType, ScannerReplacement}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; - enum ScannerReplacement { - Payable(ScannerMock), - PendingPayable(ScannerMock), - Receivable(ScannerMock), - } - impl Scanners { pub fn replace_scanner(&mut self, replacement: ScannerReplacement) { match replacement { - ScannerReplacement::Payable(scanner) => self.payable = Box::new(scanner), - ScannerReplacement::PendingPayable(scanner) => { + ScannerReplacement::Payable(ReplacementType::Real(scanner)) => { + self.payable = Box::new(scanner) + } + ScannerReplacement::Payable(ReplacementType::Mock(scanner)) => { + self.payable = Box::new(scanner) + } + ScannerReplacement::Payable(ReplacementType::Null) => { + self.payable = Box::new(NullScanner::default()) + } + ScannerReplacement::PendingPayable(ReplacementType::Real(scanner)) => { + self.pending_payable = Box::new(scanner) + } + ScannerReplacement::PendingPayable(ReplacementType::Mock(scanner)) => { self.pending_payable = Box::new(scanner) } - ScannerReplacement::Receivable(scanner) => self.receivable = Box::new(scanner), + ScannerReplacement::PendingPayable(ReplacementType::Null) => { + self.pending_payable = Box::new(NullScanner::default()) + } + ScannerReplacement::Receivable(ReplacementType::Real(scanner)) => { + self.receivable = Box::new(scanner) + } + ScannerReplacement::Receivable(ReplacementType::Mock(scanner)) => { + self.receivable = Box::new(scanner) + } + ScannerReplacement::Receivable(ReplacementType::Null) => { + self.pending_payable = Box::new(NullScanner::default()) + } } } - pub fn reset_scan_started( - &mut self, - scan_type: ScanType, - value_opt: Option, - logger: &Logger, - ) { + pub fn reset_scan_started(&mut self, scan_type: ScanType, value: MarkScanner) { match scan_type { - ScanType::Payables => Self::scanner_timestamp_treatment( - self.payable.access_scanner().scanner, - value_opt, - logger, - ), - ScanType::PendingPayables => Self::scanner_timestamp_treatment( - self.pending_payable.access_scanner().scanner, - value_opt, - logger, - ), - ScanType::Receivables => Self::scanner_timestamp_treatment( - self.receivable.access_scanner().scanner, - value_opt, - logger, - ), + ScanType::Payables => { + Self::simple_scanner_timestamp_treatment(&mut *self.payable, value) + } + ScanType::PendingPayables => { + Self::simple_scanner_timestamp_treatment(&mut *self.pending_payable, value) + } + ScanType::Receivables => { + Self::simple_scanner_timestamp_treatment(&mut *self.receivable, value) + } } } - fn scanner_timestamp_treatment( - scanner: &mut dyn Scanner, - value_opt: Option, - logger: &Logger, + fn simple_scanner_timestamp_treatment( + scanner: &mut Scanner, + value: MarkScanner, ) where - Message: actix::Message, + Scanner: self::Scanner + ?Sized, + EndMessage: actix::Message, { - match value_opt { - None => scanner.mark_as_ended(logger), - Some(timestamp) => scanner.mark_as_started(timestamp), + match value { + MarkScanner::Ended(logger) => scanner.mark_as_ended(logger), + MarkScanner::Started(timestamp) => scanner.mark_as_started(timestamp), + } + } + + pub fn scan_started_at(&self, scan_type: ScanType) -> Option { + match scan_type { + ScanType::Payables => self.payable.scan_started_at(), + ScanType::PendingPayables => self.pending_payable.scan_started_at(), + ScanType::Receivables => self.receivable.scan_started_at(), } } } @@ -2024,11 +1908,14 @@ mod tests { let mut subject = PayableScannerBuilder::new() .payable_dao(payable_dao) .build(); - let starter: PrivateScannerAccessToken = subject.access_scanner(); - let result = starter - .scanner - .start_scan(&consuming_wallet, now, None, &Logger::new("test")); + let result = Scanners::start_correct_payable_scanner::( + &mut subject, + &consuming_wallet, + now, + None, + &Logger::new("test"), + ); let is_scan_running = subject.scan_started_at().is_some(); assert_eq!(is_scan_running, false); @@ -2170,11 +2057,14 @@ mod tests { let mut subject = PayableScannerBuilder::new() .payable_dao(payable_dao) .build(); - let starter: PrivateScannerAccessToken = subject.access_scanner(); - let _ = starter - .scanner - .start_scan(&consuming_wallet, now, None, &Logger::new("test")); + let _ = Scanners::start_correct_payable_scanner::( + &mut subject, + &consuming_wallet, + now, + None, + &Logger::new("test"), + ); } #[test] @@ -2238,10 +2128,9 @@ mod tests { }; subject.mark_as_started(SystemTime::now()); - let result = subject.finish_scan(sent_payable, &logger); + let ui_msg_opt = subject.finish_scan(sent_payable, &logger); let is_scan_running = subject.scan_started_at().is_some(); - let ui_msg_opt = result.finished(); assert_eq!(ui_msg_opt, None); assert_eq!(is_scan_running, false); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); @@ -2623,11 +2512,10 @@ mod tests { response_skeleton_opt: None, }; - let result = subject.finish_scan(sent_payable, &logger); + let ui_msg_opt = subject.finish_scan(sent_payable, &logger); System::current().stop(); system.run(); - let ui_msg_opt = result.finished(); assert_eq!(ui_msg_opt, None); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); assert_eq!( @@ -3834,8 +3722,10 @@ mod tests { let result = subject.finish_scan(msg, &Logger::new(test_name)); let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); - let ui_msg_opt = result.finished(); - assert_eq!(ui_msg_opt, None); + assert_eq!( + result, + PendingPayableScanResult::NoPendingPayablesLeft(None) + ); assert_eq!( *transactions_confirmed_params, vec![vec![fingerprint_1, fingerprint_2]] @@ -3865,8 +3755,10 @@ mod tests { let result = subject.finish_scan(msg, &Logger::new(test_name)); let is_scan_running = subject.scan_started_at().is_some(); - let ui_msg_opt = result.finished(); - assert_eq!(ui_msg_opt, None); + assert_eq!( + result, + PendingPayableScanResult::NoPendingPayablesLeft(None) + ); assert_eq!(is_scan_running, false); let tlh = TestLogHandler::new(); tlh.exists_log_containing(&format!( @@ -4040,9 +3932,8 @@ mod tests { transactions: vec![], }; - let result = subject.finish_scan(msg, &Logger::new(test_name)); + let ui_msg_opt = subject.finish_scan(msg, &Logger::new(test_name)); - let ui_msg_opt = result.finished(); assert_eq!(ui_msg_opt, None); let set_start_block_params = set_start_block_params_arc.lock().unwrap(); assert_eq!(*set_start_block_params, vec![Some(4321)]); @@ -4130,13 +4021,12 @@ mod tests { }; subject.mark_as_started(SystemTime::now()); - let result = subject.finish_scan(msg, &Logger::new(test_name)); + let ui_msg_opt = subject.finish_scan(msg, &Logger::new(test_name)); let total_paid_receivable = subject .financial_statistics .borrow() .total_paid_receivable_wei; - let ui_msg_opt = result.finished(); assert_eq!(ui_msg_opt, None); assert_eq!(subject.scan_started_at(), None); assert_eq!(total_paid_receivable, 2_222_123_123 + 45_780 + 3_333_345); @@ -4295,8 +4185,8 @@ mod tests { )); } - fn assert_elapsed_time_in_mark_as_ended( - subject: &mut dyn Scanner, + fn assert_elapsed_time_in_mark_as_ended( + subject: &mut dyn Scanner, scanner_name: &str, test_name: &str, logger: &Logger, @@ -4335,21 +4225,21 @@ mod tests { let logger = Logger::new(test_name); let log_handler = TestLogHandler::new(); - assert_elapsed_time_in_mark_as_ended::( + assert_elapsed_time_in_mark_as_ended::>( &mut PayableScannerBuilder::new().build(), "Payables", test_name, &logger, &log_handler, ); - assert_elapsed_time_in_mark_as_ended::( + assert_elapsed_time_in_mark_as_ended::( &mut PendingPayableScannerBuilder::new().build(), "PendingPayables", test_name, &logger, &log_handler, ); - assert_elapsed_time_in_mark_as_ended::( + assert_elapsed_time_in_mark_as_ended::>( &mut ReceivableScannerBuilder::new().build(), "Receivables", test_name, diff --git a/node/src/accountant/scanners/payable_scanner_extension/mod.rs b/node/src/accountant/scanners/payable_scanner_extension/mod.rs index 2503d568a..1541cda7f 100644 --- a/node/src/accountant/scanners/payable_scanner_extension/mod.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/mod.rs @@ -10,24 +10,23 @@ use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; -use crate::accountant::scanners::{ - PrivateScannerWithAccessToken, PrivateStartableScannerWithAccessToken, Scanner, -}; +use crate::accountant::scanners::{Scanner, StartableScanner}; use crate::accountant::{ScanForNewPayables, ScanForRetryPayables, SentPayables}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use actix::Message; use itertools::Either; use masq_lib::logger::Logger; +use masq_lib::ui_gateway::NodeToUiMessage; -pub trait MultistageDualPayableScanner: - PrivateStartableScannerWithAccessToken - + PrivateStartableScannerWithAccessToken - + PrivateScannerWithAccessToken +pub(in crate::accountant::scanners) trait MultistageDualPayableScanner: + StartableScanner + + StartableScanner + SolvencySensitivePaymentInstructor + + Scanner> { } -pub trait SolvencySensitivePaymentInstructor { +pub(in crate::accountant::scanners) trait SolvencySensitivePaymentInstructor { fn try_skipping_payment_adjustment( &self, msg: BlockchainAgentWithContextMessage, diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 8f1cc48f6..48409d0c2 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -328,8 +328,9 @@ pub mod pending_payable_scanner_utils { } } + #[derive(Debug, PartialEq)] pub enum PendingPayableScanResult { - PendingPayablesFinished(Option), + NoPendingPayablesLeft(Option), PaymentRetryRequired, } diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index bc5cb0076..b6667a1b8 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -2,7 +2,18 @@ #![cfg(test)] +use crate::accountant::scanners::local_test_utils::ScannerMock; +use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::scan_schedulers::NewPayableScanDynIntervalComputer; +use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; +use crate::accountant::scanners::{PayableScanner, PendingPayableScanner, ReceivableScanner}; +use crate::accountant::{ + ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, SentPayables, +}; +use crate::blockchain::blockchain_bridge::RetrieveTransactions; +use itertools::Either; +use masq_lib::logger::Logger; +use masq_lib::ui_gateway::NodeToUiMessage; use std::cell::RefCell; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; @@ -43,3 +54,43 @@ impl NewPayableScanDynIntervalComputerMock { self } } + +pub enum ReplacementType { + Real(A), + Mock(B), + Null, +} + +// The supplied scanner types are broken down to these detailed categories because they are +// eventually represented by a private trait within the Scanners struct. Therefore, when +// the values are constructed, they cannot be made into a trait object right away and needs to be +// handled specifically. +pub enum ScannerReplacement { + Payable( + ReplacementType< + PayableScanner, + ScannerMock>, + >, + ), + PendingPayable( + ReplacementType< + PendingPayableScanner, + ScannerMock< + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, + >, + ), + Receivable( + ReplacementType< + ReceivableScanner, + ScannerMock>, + >, + ), +} + +pub enum MarkScanner<'a> { + Ended(&'a Logger), + Started(SystemTime), +} From 330f1279fb829507d3eae654d9f981b4a13df111 Mon Sep 17 00:00:00 2001 From: Bert Date: Sat, 10 May 2025 23:41:29 +0200 Subject: [PATCH 15/49] GH-602: todos placed (many) as understood as the whole design --- node/src/accountant/mod.rs | 6 +- node/src/accountant/scanners/mod.rs | 155 ++++++++++++------ .../scanners/payable_scanner_extension/mod.rs | 4 +- .../src/accountant/scanners/scanners_utils.rs | 15 +- node/src/accountant/scanners/test_utils.rs | 3 +- 5 files changed, 126 insertions(+), 57 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 0178b9fef..d274d591d 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1211,6 +1211,7 @@ mod tests { use std::vec; use crate::accountant::scanners::local_test_utils::{NullScanner, ScannerMock}; use crate::accountant::scanners::scan_schedulers::{NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal}; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; use crate::test_utils::recorder_counter_msgs::SingleCounterMsgSetup; @@ -2546,7 +2547,10 @@ mod tests { .started_at_result(None) .start_scan_params(&start_scan_payable_params_arc) .start_scan_result(Ok(qualified_payables_msg.clone())) - .finish_scan_result(None); + .finish_scan_result(PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::NewPendingPayable, + }); let mut config = bc_from_earning_wallet(make_wallet("hi")); config.scan_intervals_opt = Some(ScanIntervals { // This simply means that we're gonna surplus this value (it abides by how many pending diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index de296710c..3560ae2d2 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -12,12 +12,7 @@ use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ - debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_fingerprints, - investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, - separate_errors, separate_rowids_and_hashes, PayableThresholdsGauge, - PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata, -}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_fingerprints, investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, separate_errors, separate_rowids_and_hashes, OperationOutcome, PayableScanResult, PayableThresholdsGauge, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata}; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_receipt, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport, PendingPayableScanResult}; use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; use crate::accountant::{PendingPayableId, ScanError, ScanForPendingPayables, ScanForRetryPayables}; @@ -60,6 +55,7 @@ use crate::db_config::persistent_configuration::{PersistentConfiguration, Persis // Leave the individual scanner objects private! pub struct Scanners { payable: Box, + unresolved_pending_payable: bool, pending_payable: Box< dyn PrivateScanner< ScanForPendingPayables, @@ -113,6 +109,7 @@ impl Scanners { Scanners { payable, + unresolved_pending_payable: false, pending_payable, receivable, } @@ -192,6 +189,10 @@ impl Scanners { todo!() // ManualTriggerError } + if triggered_manually && !self.unresolved_pending_payable { + todo!() //ManualTriggerError + } + match ( self.pending_payable.scan_started_at(), self.payable.scan_started_at(), @@ -247,15 +248,20 @@ impl Scanners { msg: SentPayables, logger: &Logger, ) -> Option { - todo!() + let scan_result = self.payable.finish_scan(msg, logger); + match scan_result.result { + OperationOutcome::NewPendingPayable => self.unresolved_pending_payable = true, + OperationOutcome::Failure => (), + }; + scan_result.ui_response_opt } pub fn finish_pending_payable_scan( - &self, + &mut self, msg: ReportTransactionReceipts, logger: &Logger, ) -> PendingPayableScanResult { - todo!() + self.pending_payable.finish_scan(msg, logger) } pub fn finish_receivable_scan( @@ -263,7 +269,7 @@ impl Scanners { msg: ReceivedPayments, logger: &Logger, ) -> Option { - todo!() + self.receivable.finish_scan(msg, logger) } pub fn scan_error_scan_reset(&self, error: &ScanError) { @@ -332,8 +338,6 @@ trait Scanner where EndMessage: Message, { - //TODO this is an old initiative, should go away - // fn access_scanner(starter: &mut dyn StartableScanner) -> PrivateScannerAccessToken where Self: Sized; fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> ScanResult; fn scan_started_at(&self) -> Option; fn mark_as_started(&mut self, timestamp: SystemTime); @@ -466,8 +470,8 @@ impl StartableScanner for Payabl } } -impl Scanner> for PayableScanner { - fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> Option { +impl Scanner for PayableScanner { + fn finish_scan(&mut self, message: SentPayables, logger: &Logger) -> PayableScanResult { let (sent_payables, err_opt) = separate_errors(&message, logger); debug!( logger, @@ -482,12 +486,24 @@ impl Scanner> for PayableScanner { self.mark_as_ended(logger); - message - .response_skeleton_opt - .map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) + let ui_response_opt = + message + .response_skeleton_opt + .map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }); + + let result = if !sent_payables.is_empty() { + OperationOutcome::NewPendingPayable + } else { + OperationOutcome::Failure + }; + + PayableScanResult { + ui_response_opt, + result, + } } time_marking_methods!(Payables); @@ -1305,6 +1321,7 @@ impl BeginScanError { #[cfg(test)] pub mod local_test_utils { use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; use crate::accountant::scanners::{ BeginScanError, MultistageDualPayableScanner, PreparedAdjustment, PrivateScanner, Scanner, SolvencySensitivePaymentInstructor, StartableScanner, @@ -1559,12 +1576,12 @@ pub mod local_test_utils { } impl MultistageDualPayableScanner - for ScannerMock> + for ScannerMock { } impl SolvencySensitivePaymentInstructor - for ScannerMock> + for ScannerMock { fn try_skipping_payment_adjustment( &self, @@ -1600,7 +1617,7 @@ mod tests { }; use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t}; use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PendingPayableMetadata; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult, PendingPayableMetadata}; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport, PendingPayableScanResult}; use crate::accountant::scanners::{Scanner, BeginScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, PrivateScanner}; use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; @@ -1772,6 +1789,7 @@ mod tests { &payment_thresholds ); assert_eq!(payable_scanner.common.initiated_at_opt.is_some(), false); + assert_eq!(scanners.unresolved_pending_payable, false); assert_eq!( pending_payable_scanner.when_pending_too_long_sec, when_pending_too_long_sec @@ -2113,7 +2131,7 @@ mod tests { .mark_pending_payables_rowids_params(&mark_pending_payables_params_arc) .mark_pending_payables_rowids_result(Ok(())) .mark_pending_payables_rowids_result(Ok(())); - let mut subject = PayableScannerBuilder::new() + let mut payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao) .pending_payable_dao(pending_payable_dao) .build(); @@ -2126,13 +2144,19 @@ mod tests { ]), response_skeleton_opt: None, }; - subject.mark_as_started(SystemTime::now()); + payable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.payable = Box::new(payable_scanner); + let unresolved_pending_payable_before = subject.unresolved_pending_payable; - let ui_msg_opt = subject.finish_scan(sent_payable, &logger); + let node_to_ui_msg = subject.finish_payable_scan(sent_payable, &logger); - let is_scan_running = subject.scan_started_at().is_some(); - assert_eq!(ui_msg_opt, None); + let is_scan_running = subject.scan_started_at(ScanType::Payables).is_some(); + let unresolved_pending_payable_after = subject.unresolved_pending_payable; + assert_eq!(node_to_ui_msg, None); assert_eq!(is_scan_running, false); + assert_eq!(unresolved_pending_payable_before, false); + assert_eq!(unresolved_pending_payable_after, true); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); assert_eq!( *fingerprints_rowids_params, @@ -2500,7 +2524,7 @@ mod tests { }) .delete_fingerprints_params(&delete_fingerprints_params_arc) .delete_fingerprints_result(Ok(())); - let mut subject = PayableScannerBuilder::new() + let payable_scanner = PayableScannerBuilder::new() .pending_payable_dao(pending_payable_dao) .build(); let logger = Logger::new(test_name); @@ -2511,12 +2535,18 @@ mod tests { }), response_skeleton_opt: None, }; + let mut subject = make_dull_subject(); + subject.payable = Box::new(payable_scanner); + let unresolved_pending_payable_before = subject.unresolved_pending_payable; - let ui_msg_opt = subject.finish_scan(sent_payable, &logger); + let node_to_ui_msg_opt = subject.finish_payable_scan(sent_payable, &logger); + let unresolved_pending_payable_after = subject.unresolved_pending_payable; System::current().stop(); system.run(); - assert_eq!(ui_msg_opt, None); + assert_eq!(node_to_ui_msg_opt, None); + assert_eq!(unresolved_pending_payable_before, false); + assert_eq!(unresolved_pending_payable_after, false); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); assert_eq!( *fingerprints_rowids_params, @@ -2550,10 +2580,16 @@ mod tests { )), response_skeleton_opt: None, }; - let mut subject = PayableScannerBuilder::new().build(); + let payable_scanner = PayableScannerBuilder::new().build(); + let mut subject = make_dull_subject(); + subject.payable = Box::new(payable_scanner); + let unresolved_pending_payable_before = subject.unresolved_pending_payable; - subject.finish_scan(sent_payable, &Logger::new(test_name)); + subject.finish_payable_scan(sent_payable, &Logger::new(test_name)); + let unresolved_pending_payable_after = subject.unresolved_pending_payable; + assert_eq!(unresolved_pending_payable_before, false); + assert_eq!(unresolved_pending_payable_after, false); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing(&format!( "DEBUG: {test_name}: Got 0 properly sent payables of an unknown number of attempts" @@ -3668,7 +3704,7 @@ mod tests { .transactions_confirmed_params(&transactions_confirmed_params_arc) .transactions_confirmed_result(Ok(())); let pending_payable_dao = PendingPayableDaoMock::new().delete_fingerprints_result(Ok(())); - let mut subject = PendingPayableScannerBuilder::new() + let mut pending_payable_scanner = PendingPayableScannerBuilder::new() .payable_dao(payable_dao) .pending_payable_dao(pending_payable_dao) .build(); @@ -3717,9 +3753,11 @@ mod tests { ], response_skeleton_opt: None, }; - subject.mark_as_started(SystemTime::now()); + pending_payable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.pending_payable = Box::new(pending_payable_scanner); - let result = subject.finish_scan(msg, &Logger::new(test_name)); + let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); let transactions_confirmed_params = transactions_confirmed_params_arc.lock().unwrap(); assert_eq!( @@ -3730,7 +3768,7 @@ mod tests { *transactions_confirmed_params, vec![vec![fingerprint_1, fingerprint_2]] ); - assert_eq!(subject.scan_started_at(), None); + assert_eq!(subject.scan_started_at(ScanType::PendingPayables), None); TestLogHandler::new().assert_logs_match_in_order(vec![ &format!( "INFO: {}: Transactions {:?}, {:?} completed their confirmation process succeeding", @@ -3745,16 +3783,18 @@ mod tests { init_test_logging(); let test_name = "pending_payable_scanner_handles_report_transaction_receipts_message_with_empty_vector"; - let mut subject = PendingPayableScannerBuilder::new().build(); + let mut pending_payable_scanner = PendingPayableScannerBuilder::new().build(); let msg = ReportTransactionReceipts { fingerprints_with_receipts: vec![], response_skeleton_opt: None, }; - subject.mark_as_started(SystemTime::now()); + pending_payable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.pending_payable = Box::new(pending_payable_scanner); - let result = subject.finish_scan(msg, &Logger::new(test_name)); + let result = subject.finish_pending_payable_scan(msg, &Logger::new(test_name)); - let is_scan_running = subject.scan_started_at().is_some(); + let is_scan_running = subject.scan_started_at(ScanType::PendingPayables).is_some(); assert_eq!( result, PendingPayableScanResult::NoPendingPayablesLeft(None) @@ -3922,7 +3962,7 @@ mod tests { .start_block_result(Ok(None)) .set_start_block_params(&set_start_block_params_arc) .set_start_block_result(Ok(())); - let mut subject = ReceivableScannerBuilder::new() + let receivable_scanner = ReceivableScannerBuilder::new() .persistent_configuration(persistent_config) .build(); let msg = ReceivedPayments { @@ -3931,8 +3971,10 @@ mod tests { response_skeleton_opt: None, transactions: vec![], }; + let mut subject = make_dull_subject(); + subject.receivable = Box::new(receivable_scanner); - let ui_msg_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let ui_msg_opt = subject.finish_receivable_scan(msg, &Logger::new(test_name)); assert_eq!(ui_msg_opt, None); let set_start_block_params = set_start_block_params_arc.lock().unwrap(); @@ -3966,7 +4008,6 @@ mod tests { response_skeleton_opt: None, transactions: vec![], }; - // Not necessary, rather for preciseness subject.mark_as_started(SystemTime::now()); @@ -3994,13 +4035,15 @@ mod tests { let receivable_dao = ReceivableDaoMock::new() .more_money_received_params(&more_money_received_params_arc) .more_money_received_result(transaction); - let mut subject = ReceivableScannerBuilder::new() + let mut receivable_scanner = ReceivableScannerBuilder::new() .receivable_dao(receivable_dao) .persistent_configuration(persistent_config) .build(); - let mut financial_statistics = subject.financial_statistics.borrow().clone(); + let mut financial_statistics = receivable_scanner.financial_statistics.borrow().clone(); financial_statistics.total_paid_receivable_wei += 2_222_123_123; - subject.financial_statistics.replace(financial_statistics); + receivable_scanner + .financial_statistics + .replace(financial_statistics); let receivables = vec![ BlockchainTransaction { block_number: 4578910, @@ -4019,16 +4062,23 @@ mod tests { response_skeleton_opt: None, transactions: receivables.clone(), }; - subject.mark_as_started(SystemTime::now()); + receivable_scanner.mark_as_started(SystemTime::now()); + let mut subject = make_dull_subject(); + subject.receivable = Box::new(receivable_scanner); - let ui_msg_opt = subject.finish_scan(msg, &Logger::new(test_name)); + let ui_msg_opt = subject.finish_receivable_scan(msg, &Logger::new(test_name)); - let total_paid_receivable = subject + let scanner_after = subject + .receivable + .as_any() + .downcast_ref::() + .unwrap(); + let total_paid_receivable = scanner_after .financial_statistics .borrow() .total_paid_receivable_wei; assert_eq!(ui_msg_opt, None); - assert_eq!(subject.scan_started_at(), None); + assert_eq!(scanner_after.scan_started_at(), None); assert_eq!(total_paid_receivable, 2_222_123_123 + 45_780 + 3_333_345); let more_money_received_params = more_money_received_params_arc.lock().unwrap(); assert_eq!(*more_money_received_params, vec![(now, receivables)]); @@ -4225,7 +4275,7 @@ mod tests { let logger = Logger::new(test_name); let log_handler = TestLogHandler::new(); - assert_elapsed_time_in_mark_as_ended::>( + assert_elapsed_time_in_mark_as_ended::( &mut PayableScannerBuilder::new().build(), "Payables", test_name, @@ -4282,6 +4332,7 @@ mod tests { fn make_dull_subject() -> Scanners { Scanners { payable: Box::new(NullScanner::new()), + unresolved_pending_payable: false, pending_payable: Box::new(NullScanner::new()), receivable: Box::new(NullScanner::new()), } diff --git a/node/src/accountant/scanners/payable_scanner_extension/mod.rs b/node/src/accountant/scanners/payable_scanner_extension/mod.rs index 1541cda7f..6ff3ebe20 100644 --- a/node/src/accountant/scanners/payable_scanner_extension/mod.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/mod.rs @@ -10,19 +10,19 @@ use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; use crate::accountant::scanners::{Scanner, StartableScanner}; use crate::accountant::{ScanForNewPayables, ScanForRetryPayables, SentPayables}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use actix::Message; use itertools::Either; use masq_lib::logger::Logger; -use masq_lib::ui_gateway::NodeToUiMessage; pub(in crate::accountant::scanners) trait MultistageDualPayableScanner: StartableScanner + StartableScanner + SolvencySensitivePaymentInstructor - + Scanner> + + Scanner { } diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 48409d0c2..397cd434f 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -6,7 +6,7 @@ pub mod payable_scanner_utils { use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; - use crate::accountant::{comma_joined_stringifiable, SentPayables}; + use crate::accountant::{comma_joined_stringifiable, ResponseSkeleton, SentPayables}; use crate::sub_lib::accountant::PaymentThresholds; use crate::sub_lib::wallet::Wallet; use itertools::Itertools; @@ -16,6 +16,7 @@ pub mod payable_scanner_utils { use std::time::SystemTime; use thousands::Separable; use web3::types::H256; + use masq_lib::ui_gateway::NodeToUiMessage; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RpcPayableFailure}; @@ -26,6 +27,18 @@ pub mod payable_scanner_utils { RemotelyCausedErrors(Vec), } + #[derive(Debug, PartialEq)] + pub struct PayableScanResult { + pub ui_response_opt: Option, + pub result: OperationOutcome, + } + + #[derive(Debug, PartialEq)] + pub enum OperationOutcome { + NewPendingPayable, + Failure, + } + //debugging purposes only pub fn investigate_debt_extremes( timestamp: SystemTime, diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index b6667a1b8..7688ccb6b 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -5,6 +5,7 @@ use crate::accountant::scanners::local_test_utils::ScannerMock; use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::scan_schedulers::NewPayableScanDynIntervalComputer; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::accountant::scanners::{PayableScanner, PendingPayableScanner, ReceivableScanner}; use crate::accountant::{ @@ -69,7 +70,7 @@ pub enum ScannerReplacement { Payable( ReplacementType< PayableScanner, - ScannerMock>, + ScannerMock, >, ), PendingPayable( From f7377fd626a334e42eb2c9c7a60a42bf44f04492 Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 11 May 2025 22:22:34 +0200 Subject: [PATCH 16/49] GH-602: more cases covered...knocking off todos!() continues --- node/src/accountant/mod.rs | 395 ++++++++++-------- node/src/accountant/scanners/mod.rs | 10 +- .../accountant/scanners/scan_schedulers.rs | 6 + 3 files changed, 224 insertions(+), 187 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index d274d591d..8bbd1e681 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -76,7 +76,7 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; -use crate::accountant::scanners::scan_schedulers::{AutomaticSchedulingAwareScanner, ScanSchedulers}; +use crate::accountant::scanners::scan_schedulers::{AutomaticSchedulingAwareScanner, ScanScheduleHint, ScanSchedulers}; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; @@ -227,9 +227,15 @@ impl Handler for Accountant { impl Handler for Accountant { type Result = (); - fn handle(&mut self, msg: ScanForNewPayables, _ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: ScanForNewPayables, ctx: &mut Self::Context) -> Self::Result { let response_skeleton = msg.response_skeleton_opt; - self.handle_request_of_scan_for_new_payable(response_skeleton) + if self.handle_request_of_scan_for_new_payable(response_skeleton) + == ScanScheduleHint::Schedule + { + self.scan_schedulers + .payable + .schedule_for_new_payable(ctx, None) + } } } @@ -246,7 +252,11 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { - self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); + if self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt) + == ScanScheduleHint::Schedule + { + todo!() + } } } @@ -879,7 +889,7 @@ impl Accountant { fn handle_request_of_scan_for_new_payable( &mut self, response_skeleton_opt: Option, - ) { + ) -> ScanScheduleHint { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( @@ -899,6 +909,8 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); + todo!() + // ScanScheduleHint::DoNotSchedule } Err(e) => { e.handle_error( @@ -909,7 +921,7 @@ impl Accountant { //TODO simplify after having been properly tested if e == BeginScanError::NothingToProcess && response_skeleton_opt.is_none() { - todo!("schedule new scan...") + ScanScheduleHint::Schedule } else { todo!("Just hit me up") } @@ -993,7 +1005,7 @@ impl Accountant { fn handle_request_of_scan_for_receivable( &mut self, response_skeleton_opt: Option, - ) { + ) -> ScanScheduleHint { let result: Result = self.scanners.start_receivable_scan_guarded( &self.earning_wallet, @@ -1018,8 +1030,10 @@ impl Accountant { } } - if response_skeleton_opt.is_some() { + if response_skeleton_opt.is_none() { todo!("schedule") + } else { + todo!() } } @@ -1031,13 +1045,13 @@ impl Accountant { ) { match scan_type { ScanType::Payables => { - self.handle_request_of_scan_for_new_payable(Some(response_skeleton)) + self.handle_request_of_scan_for_new_payable(Some(response_skeleton)); } ScanType::PendingPayables => { - self.handle_request_of_scan_for_pending_payable(Some(response_skeleton)) + self.handle_request_of_scan_for_pending_payable(Some(response_skeleton)); } ScanType::Receivables => { - self.handle_request_of_scan_for_receivable(Some(response_skeleton)) + self.handle_request_of_scan_for_receivable(Some(response_skeleton)); } } } @@ -1414,99 +1428,6 @@ mod tests { assertions(&subject); } - #[test] - fn externally_triggered_scan_receivables_request() { - let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_millis(2_000), - receivable_scan_interval: Duration::from_millis(10_000), - }); - let receivable_dao = ReceivableDaoMock::new() - .new_delinquencies_result(vec![]) - .paid_delinquencies_result(vec![]); - let subject = AccountantBuilder::default() - .bootstrapper_config(config) - .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) - .build(); - let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let subject_addr = subject.start(); - let system = System::new("test"); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - let ui_message = NodeFromUiMessage { - client_id: 1234, - body: UiScanRequest { - scan_type: ScanType::Receivables, - } - .tmb(4321), - }; - - subject_addr.try_send(ui_message).unwrap(); - - System::current().stop(); - system.run(); - let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!( - blockchain_bridge_recording.get_record::(0), - &RetrieveTransactions { - recipient: make_wallet("earning_wallet"), - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }), - } - ); - } - - #[test] - fn received_payments_with_response_skeleton_sends_response_to_ui_gateway() { - let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_millis(2_000), - receivable_scan_interval: Duration::from_millis(10_000), - }); - config.automatic_scans_enabled = false; - let subject = AccountantBuilder::default() - .bootstrapper_config(config) - .config_dao( - ConfigDaoMock::new() - .get_result(Ok(ConfigDaoRecord::new("start_block", None, false))) - .set_result(Ok(())), - ) - .build(); - let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); - let subject_addr = subject.start(); - let system = System::new("test"); - let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - let received_payments = ReceivedPayments { - timestamp: SystemTime::now(), - new_start_block: BlockMarker::Value(0), - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }), - transactions: vec![], - }; - - subject_addr.try_send(received_payments).unwrap(); - - System::current().stop(); - system.run(); - let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); - assert_eq!( - ui_gateway_recording.get_record::(0), - &NodeToUiMessage { - target: ClientId(1234), - body: UiScanResponse {}.tmb(4321), - } - ); - } - #[test] fn externally_triggered_scan_payables_request() { let config = bc_from_earning_wallet(make_wallet("some_wallet_address")); @@ -1558,57 +1479,6 @@ mod tests { ); } - #[test] - fn external_scan_payables_request_does_not_schedule_scan_for_new_payables_if_payables_empty() { - todo!("write me up"); - let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - config.automatic_scans_enabled = false; - let fingerprint = PendingPayableFingerprint { - rowid: 1234, - timestamp: SystemTime::now(), - hash: Default::default(), - attempt: 1, - amount: 1_000_000, - process_error: None, - }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![fingerprint.clone()]); - let mut subject = AccountantBuilder::default() - .consuming_wallet(make_paying_wallet(b"consuming")) - .bootstrapper_config(config) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) - .build(); - let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let blockchain_bridge = blockchain_bridge - .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)); - let blockchain_bridge_addr = blockchain_bridge.start(); - subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); - let subject_addr = subject.start(); - let system = System::new("test"); - let ui_message = NodeFromUiMessage { - client_id: 1234, - body: UiScanRequest { - scan_type: ScanType::Payables, - } - .tmb(4321), - }; - - subject_addr.try_send(ui_message).unwrap(); - - system.run(); - let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!( - blockchain_bridge_recording.get_record::(0), - &RequestTransactionReceipts { - pending_payable: vec![fingerprint], - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }), - } - ); - } - #[test] fn sent_payable_with_response_skeleton_sends_scan_response_to_ui_gateway() { let config = bc_from_earning_wallet(make_wallet("earning_wallet")); @@ -1654,13 +1524,12 @@ mod tests { } #[test] - fn received_balances_and_qualified_payables_under_our_money_limit_thus_all_forwarded_to_blockchain_bridge( - ) { - // the numbers for balances don't do real math, they need not to match either the condition for + fn qualified_payables_under_our_money_limit_are_forwarded_to_blockchain_bridge_right_away() { + // The numbers in balances don't do real math, they don't need to match either the condition for // the payment adjustment or the actual values that come from the payable size reducing algorithm; // all that is mocked in this test init_test_logging(); - let test_name = "received_balances_and_qualified_payables_under_our_money_limit_thus_all_forwarded_to_blockchain_bridge"; + let test_name = "qualified_payables_under_our_money_limit_are_forwarded_to_blockchain_bridge_right_away"; let is_adjustment_required_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let instructions_recipient = blockchain_bridge @@ -1738,7 +1607,7 @@ mod tests { ); assert_eq!(blockchain_bridge_recording.len(), 1); test_use_of_the_same_logger(&logger_clone, test_name) - // adjust_payments() did not need a prepared result which means it wasn't reached + // adjust_payments() did not need a prepared result, which means it wasn't reached // because otherwise this test would've panicked } @@ -1751,13 +1620,13 @@ mod tests { } #[test] - fn received_qualified_payables_exceeding_our_masq_balance_are_adjusted_before_forwarded_to_blockchain_bridge( - ) { - // the numbers for balances don't do real math, they need not to match either the condition for + fn qualified_payables_over_masq_balance_are_adjusted_before_sending_to_blockchain_bridge() { + // The numbers in balances don't do real math, they don't need to match either the condition for // the payment adjustment or the actual values that come from the payable size reducing algorithm; // all that is mocked in this test init_test_logging(); - let test_name = "received_qualified_payables_exceeding_our_masq_balance_are_adjusted_before_forwarded_to_blockchain_bridge"; + let test_name = + "qualified_payables_over_masq_balance_are_adjusted_before_sending_to_blockchain_bridge"; let adjust_payments_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let report_recipient = blockchain_bridge @@ -1974,11 +1843,21 @@ mod tests { assert_eq!(blockchain_bridge_recording.len(), 1); } - // TODO This should probably become a test where the finish_scan method in pending payable scanner - // will throw an error and so the scan for payables is not gonna continue. Maybe include - // a response skeleton #[test] - fn report_transaction_receipts_with_response_skeleton_sends_scan_response_to_ui_gateway() { + fn externally_triggered_scan_for_payables_is_prevented_by_ongoing_pending_payable_scan_sequence( + ) { + todo!("write me up") + } + + #[test] + fn externally_triggered_scan_for_pending_payables_is_prevented_if_all_payments_already_complete( + ) { + todo!("write me up") + } + + #[test] + fn pending_payable_scan_response_is_sent_to_ui_gateway_when_both_participating_scanners_have_completed( + ) { todo!("fix me"); let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); config.scan_intervals_opt = Some(ScanIntervals { @@ -2082,6 +1961,62 @@ mod tests { ); } + #[test] + fn automatic_scan_for_new_payables_schedules_another_one_immediately_if_no_qualified_payables_found( + ) { + let notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let system = + System::new("automatic_scan_for_new_payables_schedules_another_one_immediately_if_no_qualified_payables_found"); + let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = AccountantBuilder::default() + .consuming_wallet(consuming_wallet) + .build(); + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(¬ify_later_params_arc), + ); + subject.scan_schedulers.payable.dyn_interval_computer = Box::new( + NewPayableScanDynIntervalComputerMock::default() + .compute_interval_results(Some(Duration::from_secs(500))), + ); + let payable_scanner = ScannerMock::default() + .scan_started_at_result(None) + .scan_started_at_result(None) + .start_scan_result(Err(BeginScanError::NothingToProcess)); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); + let accountant_addr = subject.start(); + + accountant_addr + .try_send(ScanForNewPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + System::current().stop(); + assert_eq!(system.run(), 0); + let mut notify_later_params = notify_later_params_arc.lock().unwrap(); + let (msg, interval) = notify_later_params.remove(0); + assert_eq!( + msg, + ScanForNewPayables { + response_skeleton_opt: None + } + ); + assert_eq!(interval, Duration::from_secs(500)); + assert_eq!(notify_later_params.len(), 0); + // Accountant is unbound; therefore, it is guaranteed that sending a message to + // the BlockchainBridge wasn't attempted. It would've panicked otherwise. + } + #[test] fn accountant_handles_scan_for_retry_payables() { init_test_logging(); @@ -2100,7 +2035,7 @@ mod tests { response_skeleton_opt: None, }; let payable_scanner_mock = ScannerMock::new() - .started_at_result(None) + .scan_started_at_result(None) .start_scan_params(&start_scan_params_arc) .start_scan_result(Ok(qualified_payables_msg.clone())); subject @@ -2197,6 +2132,99 @@ mod tests { ); } + #[test] + fn externally_triggered_scan_receivables_request() { + let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_millis(2_000), + receivable_scan_interval: Duration::from_millis(10_000), + }); + let receivable_dao = ReceivableDaoMock::new() + .new_delinquencies_result(vec![]) + .paid_delinquencies_result(vec![]); + let subject = AccountantBuilder::default() + .bootstrapper_config(config) + .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) + .build(); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let subject_addr = subject.start(); + let system = System::new("test"); + let peer_actors = peer_actors_builder() + .blockchain_bridge(blockchain_bridge) + .build(); + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + let ui_message = NodeFromUiMessage { + client_id: 1234, + body: UiScanRequest { + scan_type: ScanType::Receivables, + } + .tmb(4321), + }; + + subject_addr.try_send(ui_message).unwrap(); + + System::current().stop(); + system.run(); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + assert_eq!( + blockchain_bridge_recording.get_record::(0), + &RetrieveTransactions { + recipient: make_wallet("earning_wallet"), + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 1234, + context_id: 4321, + }), + } + ); + } + + #[test] + fn received_payments_with_response_skeleton_sends_response_to_ui_gateway() { + let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); + config.scan_intervals_opt = Some(ScanIntervals { + payable_scan_interval: Duration::from_millis(10_000), + pending_payable_scan_interval: Duration::from_millis(2_000), + receivable_scan_interval: Duration::from_millis(10_000), + }); + config.automatic_scans_enabled = false; + let subject = AccountantBuilder::default() + .bootstrapper_config(config) + .config_dao( + ConfigDaoMock::new() + .get_result(Ok(ConfigDaoRecord::new("start_block", None, false))) + .set_result(Ok(())), + ) + .build(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let subject_addr = subject.start(); + let system = System::new("test"); + let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + let received_payments = ReceivedPayments { + timestamp: SystemTime::now(), + new_start_block: BlockMarker::Value(0), + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 1234, + context_id: 4321, + }), + transactions: vec![], + }; + + subject_addr.try_send(received_payments).unwrap(); + + System::current().stop(); + system.run(); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + assert_eq!( + ui_gateway_recording.get_record::(0), + &NodeToUiMessage { + target: ClientId(1234), + body: UiScanResponse {}.tmb(4321), + } + ); + } + #[test] fn accountant_processes_msg_with_received_payments_using_receivables_dao_and_then_updates_start_block( ) { @@ -2268,8 +2296,11 @@ mod tests { #[test] fn accountant_scans_after_startup() { - // Note: We don't assert on the payable scanner itself because it follows the pending - // payable scanner which is beyond the scope of this test + todo!("fix me....it should be like \ + 1) scan for pending payables....and when it ends, it should call the scan for new payables afterward, automatically.\ + 2) scan for receivables"); + // Note: We don't assert on the payable scanner this time because it follows the pending + // payable scanner and is beyond the scope of this test init_test_logging(); let pending_payable_params_arc = Arc::new(Mutex::new(vec![])); let new_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); @@ -2332,8 +2363,6 @@ mod tests { assert_eq!(paid_delinquencies_params[0], PaymentThresholds::default()); let scan_for_new_payables_notify_params = scan_for_new_payables_notify_params_arc.lock().unwrap(); - let scan_for_receivables_notify_later_params = - scan_for_receivables_notify_later_params_arc.lock().unwrap(); let default_scan_intervals = ScanIntervals::default(); assert_eq!( *scan_for_new_payables_notify_params, @@ -2350,6 +2379,8 @@ mod tests { "Was meant to be empty but contained: {:?}", scan_for_new_payables_notify_later_params ); + let scan_for_receivables_notify_later_params = + scan_for_receivables_notify_later_params_arc.lock().unwrap(); assert_eq!( *scan_for_receivables_notify_later_params, vec![( @@ -2377,8 +2408,8 @@ mod tests { let system = System::new(test_name); SystemKillerActor::new(Duration::from_secs(10)).start(); // a safety net for GitHub Actions let receivable_scanner = ScannerMock::new() - .started_at_result(None) - .started_at_result(None) + .scan_started_at_result(None) + .scan_started_at_result(None) .start_scan_params(&start_scan_params_arc) .start_scan_result(Err(BeginScanError::NothingToProcess)) .start_scan_result(Ok(RetrieveTransactions { @@ -2532,7 +2563,7 @@ mod tests { response_skeleton_opt: None, }; let pending_payable_scanner = ScannerMock::new() - .started_at_result(None) + .scan_started_at_result(None) .start_scan_params(&start_scan_pending_payable_params_arc) .start_scan_result(Ok(request_transaction_receipts.clone())) .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); @@ -2542,9 +2573,9 @@ mod tests { response_skeleton_opt: None, }; let payable_scanner = ScannerMock::new() - .started_at_result(None) + .scan_started_at_result(None) // Always checking also on the payable scanner when handling ScanForPendingPayable - .started_at_result(None) + .scan_started_at_result(None) .start_scan_params(&start_scan_payable_params_arc) .start_scan_result(Ok(qualified_payables_msg.clone())) .finish_scan_result(PayableScanResult { @@ -2746,7 +2777,7 @@ mod tests { System::current().stop(); assert_eq!(system.run(), 0); - // no panics because of recalcitrant DAOs; therefore DAOs were not called; therefore test passes + // No panics because of recalcitrant DAOs; therefore DAOs were not called; therefore test passes TestLogHandler::new().exists_log_containing( &format!("{test_name}: Started with --scans off; declining to begin database and blockchain scans"), ); @@ -2832,7 +2863,7 @@ mod tests { } #[test] - fn scan_for_payable_message_triggers_payment_for_balances_over_the_curve() { + fn scan_for_new_payables_triggers_payment_for_balances_over_the_curve() { init_test_logging(); let mut config = bc_from_earning_wallet(make_wallet("mine")); let consuming_wallet = make_paying_wallet(b"consuming"); @@ -2962,7 +2993,7 @@ mod tests { // We ignored the second ScanForNewPayables message as there was already in progress from // the first message. Now we reset the state by ending the first scan by a failure and see - // that the third scan request is gonna be accepted willingly again. + // that the third scan request is going to be accepted willingly again. addr.try_send(SentPayables { payment_procedure_result: Err(PayableTransactionError::Signing("bluh".to_string())), response_skeleton_opt: None, @@ -4984,7 +5015,7 @@ mod tests { }) .unwrap(); System::current().stop(); - system.run(); + assert_eq!(system.run(), 0); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); match message.response_skeleton_opt { Some(response_skeleton) => { diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 3560ae2d2..164f97e33 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1428,7 +1428,7 @@ pub mod local_test_utils { start_scan_results: RefCell>>, finish_scan_params: Arc>>, finish_scan_results: RefCell>, - started_at_results: RefCell>>, + scan_started_at_results: RefCell>>, stop_system_after_last_message: RefCell, } @@ -1488,7 +1488,7 @@ pub mod local_test_utils { } fn scan_started_at(&self) -> Option { - self.started_at_results.borrow_mut().remove(0) + self.scan_started_at_results.borrow_mut().remove(0) } fn mark_as_started(&mut self, _timestamp: SystemTime) { @@ -1515,7 +1515,7 @@ pub mod local_test_utils { start_scan_results: RefCell::new(vec![]), finish_scan_params: Arc::new(Mutex::new(vec![])), finish_scan_results: RefCell::new(vec![]), - started_at_results: RefCell::new(vec![]), + scan_started_at_results: RefCell::new(vec![]), stop_system_after_last_message: RefCell::new(false), } } @@ -1533,8 +1533,8 @@ pub mod local_test_utils { self } - pub fn started_at_result(self, result: Option) -> Self { - self.started_at_results.borrow_mut().push(result); + pub fn scan_started_at_result(self, result: Option) -> Self { + self.scan_started_at_results.borrow_mut().push(result); self } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 464f81578..28983e12e 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -42,6 +42,12 @@ pub enum PayableScanSchedulerError { ScanForNewPayableAlreadyScheduled, } +#[derive(Debug, PartialEq)] +pub enum ScanScheduleHint { + Schedule, + DoNotSchedule, +} + pub struct PayableScanScheduler { pub new_payable_notify_later: Box>, pub dyn_interval_computer: Box, From 2c447b4764d64aa664203113d3ea927487217dd5 Mon Sep 17 00:00:00 2001 From: Bert Date: Wed, 14 May 2025 15:39:31 +0200 Subject: [PATCH 17/49] GH-602: made a markup for upcoming steps --- node/src/accountant/mod.rs | 69 +++++++++---------- node/src/accountant/scanners/mod.rs | 61 ++++++++-------- .../accountant/scanners/scan_schedulers.rs | 2 - 3 files changed, 63 insertions(+), 69 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 8bbd1e681..6b0866b93 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -219,6 +219,10 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { + // By this time we know it is an automatic scanner, which may or may not be rescheduled. + // It depends on the findings: if it finds failed transactions, then it will launch + // the RetryPayableScanner, which finishes, and the PendingPayablesScanner is scheduled + // to run again. Therefore, not from here. let response_skeleton_opt = msg.response_skeleton_opt; self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) } @@ -228,6 +232,11 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForNewPayables, ctx: &mut Self::Context) -> Self::Result { + // We know this must be a scheduled scanner, but we don't if we are going to reschedule it + // from here or elsewhere. If the scan finds no payables that qualify for a payment, we do + // it right away. If some new pending payables are produced, the next scheduling is going to + // be determined by the PendingPayableScanner, evaluating if it has seen all pending + // payables complete. That opens up an opportunity for another run of the NewPayableScanner. let response_skeleton = msg.response_skeleton_opt; if self.handle_request_of_scan_for_new_payable(response_skeleton) == ScanScheduleHint::Schedule @@ -243,6 +252,8 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForRetryPayables, _ctx: &mut Self::Context) -> Self::Result { + // RetryPayableScanner is scheduled only when the PendingPayableScanner finishes finding out + // that there have been some failed pending payables. That means not from here. let response_skeleton = msg.response_skeleton_opt; self.handle_request_of_scan_for_retry_payable(response_skeleton); } @@ -252,11 +263,10 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { - if self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt) - == ScanScheduleHint::Schedule - { - todo!() - } + // By this time we know it is an automatic scanner, which is always rescheduled right away, + // no matter what its outcome is. + self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); + todo!("receivable scanner periodical") } } @@ -273,11 +283,11 @@ impl Handler for Accountant { .expect("UIGateway is not bound") .try_send(node_to_ui_msg) .expect("UIGateway is dead"); - todo!(); - // Externally triggered scans are not allowed to unwind beyond their own scope; - // only the very action of the specified scan is performed. + todo!("Finishing PendingPayable scan. Non-automatic."); + // Externally triggered scan is not allowed to be a spark for a procedure that + // would involve payables with fresh nonces. The job is done. } else { - todo!(); + todo!("Finishing PendingPayable scan. Automatic scanning."); self.scan_schedulers .payable .schedule_for_new_payable(ctx, response_skeleton_opt) @@ -309,28 +319,24 @@ impl Handler for Accountant { fn handle(&mut self, msg: SentPayables, ctx: &mut Self::Context) -> Self::Result { match self.scanners.finish_payable_scan(msg, &self.logger) { None => { - todo!() //self.scan_schedulers.pending_payable.schedule(ctx, None) + todo!("Finishing automatic payable scan") //self.scan_schedulers.pending_payable.schedule(ctx, None) } - // TODO merge after tested in and out - Some(node_to_ui_msg) if self.scan_schedulers.automatic_scans_enabled => { - todo!(); - self.ui_message_sub_opt - .as_ref() - .expect("UIGateway is not bound") - .try_send(node_to_ui_msg) - .expect("UIGateway is dead"); - } - + // TODO Might be worth a consideration with GH-635 + // Some(node_to_ui_msg) if self.scan_schedulers.automatic_scans_enabled => { + // todo!(); + // } Some(node_to_ui_msg) => { - todo!("Test externally triggered msgs don't chain it up...if not..."); + todo!("Externally triggered payable scan is finishing"); self.ui_message_sub_opt .as_ref() .expect("UIGateway is not bound") .try_send(node_to_ui_msg) .expect("UIGateway is dead"); - // Externally triggered scans should not provoke a sequence spread by intervals, that is - // exclusive to automatic (scheduled respectively) scanning processes. + // When automatic scans are suppressed, the external triggers are not allowed to + // provoke a scan sequence spread across intervals. The only exception is + // the PendingPayableScanner and RetryPayableScanner, which are meant to run in + // tandem. } } } @@ -897,7 +903,6 @@ impl Accountant { SystemTime::now(), response_skeleton_opt, &self.logger, - self.scan_schedulers.pending_payable_sequence_in_process, ), None => Err(BeginScanError::NoConsumingWalletFound), }; @@ -1005,7 +1010,7 @@ impl Accountant { fn handle_request_of_scan_for_receivable( &mut self, response_skeleton_opt: Option, - ) -> ScanScheduleHint { + ) { let result: Result = self.scanners.start_receivable_scan_guarded( &self.earning_wallet, @@ -1029,12 +1034,6 @@ impl Accountant { ); } } - - if response_skeleton_opt.is_none() { - todo!("schedule") - } else { - todo!() - } } fn handle_externally_triggered_scan( @@ -1043,6 +1042,8 @@ impl Accountant { scan_type: ScanType, response_skeleton: ResponseSkeleton, ) { + // Each of these scans runs only once per request, they do not go on into a sequence under + // any circumstances match scan_type { ScanType::Payables => { self.handle_request_of_scan_for_new_payable(Some(response_skeleton)); @@ -1843,12 +1844,6 @@ mod tests { assert_eq!(blockchain_bridge_recording.len(), 1); } - #[test] - fn externally_triggered_scan_for_payables_is_prevented_by_ongoing_pending_payable_scan_sequence( - ) { - todo!("write me up") - } - #[test] fn externally_triggered_scan_for_pending_payables_is_prevented_if_all_payments_already_complete( ) { diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 164f97e33..476d8d1ab 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -55,7 +55,7 @@ use crate::db_config::persistent_configuration::{PersistentConfiguration, Persis // Leave the individual scanner objects private! pub struct Scanners { payable: Box, - unresolved_pending_payable: bool, + aware_of_unresolved_pending_payable: bool, pending_payable: Box< dyn PrivateScanner< ScanForPendingPayables, @@ -109,7 +109,7 @@ impl Scanners { Scanners { payable, - unresolved_pending_payable: false, + aware_of_unresolved_pending_payable: false, pending_payable, receivable, } @@ -121,14 +121,14 @@ impl Scanners { timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - pending_payable_sequence_in_process: bool, + automatic_scans_enabled: bool, ) -> Result { let triggered_manually = response_skeleton_opt.is_some(); // Under normal circumstances, it is guaranteed that the new-payable scanner will never // overlap with the execution of the pending payable scanner, as they are safely // and automatically scheduled to run sequentially. However, since we allow for unexpected, // manually triggered scans, a conflict is possible and must be handled. - if triggered_manually && pending_payable_sequence_in_process { + if triggered_manually && automatic_scans_enabled { todo!() // ManualTriggerError } @@ -182,15 +182,16 @@ impl Scanners { timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - pending_payable_sequence_in_process: bool, + automatic_scans_enabled: bool, ) -> Result { let triggered_manually = response_skeleton_opt.is_some(); - if triggered_manually && pending_payable_sequence_in_process { - todo!() // ManualTriggerError - } - if triggered_manually && !self.unresolved_pending_payable { - todo!() //ManualTriggerError + if triggered_manually && automatic_scans_enabled { + todo!("forbidden") + } else if triggered_manually && !self.aware_of_unresolved_pending_payable { + todo!("useless") + } else if !self.aware_of_unresolved_pending_payable { + todo!("unreachable") } match ( @@ -198,12 +199,12 @@ impl Scanners { self.payable.scan_started_at(), ) { (Some(pp_timestamp), Some(p_timestamp)) => - // If you're wondering, then yes, this should be sacre in the relationship between - // Pending Payable and New Payable. + // If you're wondering, then yes, this should be the sacre truth between + // PendingPayableScanner and NewPayableScanner. { unreachable!( - "Both payable scanners should never be allowed to run in parallel. \ - Scan for pending payables started at: {}, scan for payables started at: {}", + "Both payable scanners should never be allowed to run in parallel. Scan for \ + pending payables started at: {}, scan for payables started at: {}", BeginScanError::timestamp_as_string(pp_timestamp), BeginScanError::timestamp_as_string(p_timestamp) ) @@ -250,7 +251,7 @@ impl Scanners { ) -> Option { let scan_result = self.payable.finish_scan(msg, logger); match scan_result.result { - OperationOutcome::NewPendingPayable => self.unresolved_pending_payable = true, + OperationOutcome::NewPendingPayable => self.aware_of_unresolved_pending_payable = true, OperationOutcome::Failure => (), }; scan_result.ui_response_opt @@ -273,7 +274,7 @@ impl Scanners { } pub fn scan_error_scan_reset(&self, error: &ScanError) { - todo!() + todo!("test me locally") } pub fn try_skipping_payable_adjustment( @@ -1789,7 +1790,7 @@ mod tests { &payment_thresholds ); assert_eq!(payable_scanner.common.initiated_at_opt.is_some(), false); - assert_eq!(scanners.unresolved_pending_payable, false); + assert_eq!(scanners.aware_of_unresolved_pending_payable, false); assert_eq!( pending_payable_scanner.when_pending_too_long_sec, when_pending_too_long_sec @@ -2147,16 +2148,16 @@ mod tests { payable_scanner.mark_as_started(SystemTime::now()); let mut subject = make_dull_subject(); subject.payable = Box::new(payable_scanner); - let unresolved_pending_payable_before = subject.unresolved_pending_payable; + let aware_of_unresolved_pending_payable_before = subject.aware_of_unresolved_pending_payable; let node_to_ui_msg = subject.finish_payable_scan(sent_payable, &logger); let is_scan_running = subject.scan_started_at(ScanType::Payables).is_some(); - let unresolved_pending_payable_after = subject.unresolved_pending_payable; + let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; assert_eq!(node_to_ui_msg, None); assert_eq!(is_scan_running, false); - assert_eq!(unresolved_pending_payable_before, false); - assert_eq!(unresolved_pending_payable_after, true); + assert_eq!(aware_of_unresolved_pending_payable_before, false); + assert_eq!(aware_of_unresolved_pending_payable_after, true); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); assert_eq!( *fingerprints_rowids_params, @@ -2537,16 +2538,16 @@ mod tests { }; let mut subject = make_dull_subject(); subject.payable = Box::new(payable_scanner); - let unresolved_pending_payable_before = subject.unresolved_pending_payable; + let aware_of_unresolved_pending_payable_before = subject.aware_of_unresolved_pending_payable; let node_to_ui_msg_opt = subject.finish_payable_scan(sent_payable, &logger); - let unresolved_pending_payable_after = subject.unresolved_pending_payable; + let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; System::current().stop(); system.run(); assert_eq!(node_to_ui_msg_opt, None); - assert_eq!(unresolved_pending_payable_before, false); - assert_eq!(unresolved_pending_payable_after, false); + assert_eq!(aware_of_unresolved_pending_payable_before, false); + assert_eq!(aware_of_unresolved_pending_payable_after, false); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); assert_eq!( *fingerprints_rowids_params, @@ -2583,13 +2584,13 @@ mod tests { let payable_scanner = PayableScannerBuilder::new().build(); let mut subject = make_dull_subject(); subject.payable = Box::new(payable_scanner); - let unresolved_pending_payable_before = subject.unresolved_pending_payable; + let aware_of_unresolved_pending_payable_before = subject.aware_of_unresolved_pending_payable; subject.finish_payable_scan(sent_payable, &Logger::new(test_name)); - let unresolved_pending_payable_after = subject.unresolved_pending_payable; - assert_eq!(unresolved_pending_payable_before, false); - assert_eq!(unresolved_pending_payable_after, false); + let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; + assert_eq!(aware_of_unresolved_pending_payable_before, false); + assert_eq!(aware_of_unresolved_pending_payable_after, false); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing(&format!( "DEBUG: {test_name}: Got 0 properly sent payables of an unknown number of attempts" @@ -4332,7 +4333,7 @@ mod tests { fn make_dull_subject() -> Scanners { Scanners { payable: Box::new(NullScanner::new()), - unresolved_pending_payable: false, + aware_of_unresolved_pending_payable: false, pending_payable: Box::new(NullScanner::new()), receivable: Box::new(NullScanner::new()), } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 28983e12e..7d0817e2a 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -19,7 +19,6 @@ pub struct ScanSchedulers { pub payable: PayableScanScheduler, pub pending_payable: SimplePeriodicalScanScheduler, pub receivable: SimplePeriodicalScanScheduler, - pub pending_payable_sequence_in_process: bool, pub automatic_scans_enabled: bool, } @@ -31,7 +30,6 @@ impl ScanSchedulers { scan_intervals.pending_payable_scan_interval, ), receivable: SimplePeriodicalScanScheduler::new(scan_intervals.receivable_scan_interval), - pending_payable_sequence_in_process: false, automatic_scans_enabled, } } From f362cf81fb0590998ed1102f593d126bc5b7a379 Mon Sep 17 00:00:00 2001 From: Bert Date: Wed, 14 May 2025 15:39:31 +0200 Subject: [PATCH 18/49] GH-602: made a markup for upcoming steps --- node/src/accountant/mod.rs | 95 ++++++++++--------- node/src/accountant/scanners/mod.rs | 71 +++++++------- .../accountant/scanners/scan_schedulers.rs | 3 - 3 files changed, 85 insertions(+), 84 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 8bbd1e681..4301b1114 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -219,6 +219,10 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { + // By this time we know it is an automatic scanner, which may or may not be rescheduled. + // It depends on the findings: if it finds failed transactions, then it will launch + // the RetryPayableScanner, which finishes, and the PendingPayablesScanner is scheduled + // to run again. Therefore, not from here. let response_skeleton_opt = msg.response_skeleton_opt; self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) } @@ -228,6 +232,11 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForNewPayables, ctx: &mut Self::Context) -> Self::Result { + // We know this must be a scheduled scanner, but we don't if we are going to reschedule it + // from here or elsewhere. If the scan finds no payables that qualify for a payment, we do + // it right away. If some new pending payables are produced, the next scheduling is going to + // be determined by the PendingPayableScanner, evaluating if it has seen all pending + // payables complete. That opens up an opportunity for another run of the NewPayableScanner. let response_skeleton = msg.response_skeleton_opt; if self.handle_request_of_scan_for_new_payable(response_skeleton) == ScanScheduleHint::Schedule @@ -243,6 +252,8 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForRetryPayables, _ctx: &mut Self::Context) -> Self::Result { + // RetryPayableScanner is scheduled only when the PendingPayableScanner finishes finding out + // that there have been some failed pending payables. That means not from here. let response_skeleton = msg.response_skeleton_opt; self.handle_request_of_scan_for_retry_payable(response_skeleton); } @@ -252,11 +263,10 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { - if self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt) - == ScanScheduleHint::Schedule - { - todo!() - } + // By this time we know it is an automatic scanner, which is always rescheduled right away, + // no matter what its outcome is. + self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); + todo!("receivable scanner periodical") } } @@ -273,11 +283,11 @@ impl Handler for Accountant { .expect("UIGateway is not bound") .try_send(node_to_ui_msg) .expect("UIGateway is dead"); - todo!(); - // Externally triggered scans are not allowed to unwind beyond their own scope; - // only the very action of the specified scan is performed. + todo!("Finishing PendingPayable scan. Non-automatic."); + // Externally triggered scan is not allowed to be a spark for a procedure that + // would involve payables with fresh nonces. The job is done. } else { - todo!(); + todo!("Finishing PendingPayable scan. Automatic scanning."); self.scan_schedulers .payable .schedule_for_new_payable(ctx, response_skeleton_opt) @@ -309,28 +319,24 @@ impl Handler for Accountant { fn handle(&mut self, msg: SentPayables, ctx: &mut Self::Context) -> Self::Result { match self.scanners.finish_payable_scan(msg, &self.logger) { None => { - todo!() //self.scan_schedulers.pending_payable.schedule(ctx, None) + todo!("Finishing automatic payable scan") //self.scan_schedulers.pending_payable.schedule(ctx, None) } - // TODO merge after tested in and out - Some(node_to_ui_msg) if self.scan_schedulers.automatic_scans_enabled => { - todo!(); - self.ui_message_sub_opt - .as_ref() - .expect("UIGateway is not bound") - .try_send(node_to_ui_msg) - .expect("UIGateway is dead"); - } - + // TODO Might be worth a consideration with GH-635 + // Some(node_to_ui_msg) if self.scan_schedulers.automatic_scans_enabled => { + // todo!(); + // } Some(node_to_ui_msg) => { - todo!("Test externally triggered msgs don't chain it up...if not..."); + todo!("Externally triggered payable scan is finishing"); self.ui_message_sub_opt .as_ref() .expect("UIGateway is not bound") .try_send(node_to_ui_msg) .expect("UIGateway is dead"); - // Externally triggered scans should not provoke a sequence spread by intervals, that is - // exclusive to automatic (scheduled respectively) scanning processes. + // When automatic scans are suppressed, the external triggers are not allowed to + // provoke a scan sequence spread across intervals. The only exception is + // the PendingPayableScanner and RetryPayableScanner, which are meant to run in + // tandem. } } } @@ -897,7 +903,7 @@ impl Accountant { SystemTime::now(), response_skeleton_opt, &self.logger, - self.scan_schedulers.pending_payable_sequence_in_process, + self.scan_schedulers.automatic_scans_enabled ), None => Err(BeginScanError::NoConsumingWalletFound), }; @@ -941,7 +947,7 @@ impl Accountant { response_skeleton_opt, &self.logger, ), - None => todo!(), //Err(BeginScanError::NoConsumingWalletFound), + None => todo!("I need a test here"), //Err(BeginScanError::NoConsumingWalletFound), }; match result { @@ -952,7 +958,7 @@ impl Accountant { .try_send(scan_message) .expect("BlockchainBridge is dead"); } - Err(e) => todo!() + Err(e) => todo!("the same test here pls!!!") // e.handle_error( // &self.logger, // ScanType::Payables, @@ -972,7 +978,7 @@ impl Accountant { SystemTime::now(), response_skeleton_opt, &self.logger, - self.scan_schedulers.pending_payable_sequence_in_process, + self.scan_schedulers.automatic_scans_enabled, ), None => Err(BeginScanError::NoConsumingWalletFound), }; @@ -991,13 +997,6 @@ impl Accountant { ScanType::PendingPayables, response_skeleton_opt.is_some(), ); - - // TODO hang on!! this can happen only with external triggers (if we get up here it - // means this was preceded by the NewPayable or the RetryPayable scans, both always - // producing at least one payment) - if e == BeginScanError::NothingToProcess { - todo!() - } } } } @@ -1005,7 +1004,7 @@ impl Accountant { fn handle_request_of_scan_for_receivable( &mut self, response_skeleton_opt: Option, - ) -> ScanScheduleHint { + ) { let result: Result = self.scanners.start_receivable_scan_guarded( &self.earning_wallet, @@ -1029,12 +1028,6 @@ impl Accountant { ); } } - - if response_skeleton_opt.is_none() { - todo!("schedule") - } else { - todo!() - } } fn handle_externally_triggered_scan( @@ -1043,6 +1036,8 @@ impl Accountant { scan_type: ScanType, response_skeleton: ResponseSkeleton, ) { + // Each of these scans runs only once per request, they do not go on into a sequence under + // any circumstances match scan_type { ScanType::Payables => { self.handle_request_of_scan_for_new_payable(Some(response_skeleton)); @@ -1223,10 +1218,12 @@ mod tests { use std::sync::Mutex; use std::time::Duration; use std::vec; + use nix::sys::socket::sockopt::BindToDevice; use crate::accountant::scanners::local_test_utils::{NullScanner, ScannerMock}; use crate::accountant::scanners::scan_schedulers::{NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; + use crate::sub_lib::tokio_wrappers::TokioListenerWrapperFactoryReal; use crate::test_utils::recorder_counter_msgs::SingleCounterMsgSetup; impl Handler> for Accountant { @@ -1844,9 +1841,19 @@ mod tests { } #[test] - fn externally_triggered_scan_for_payables_is_prevented_by_ongoing_pending_payable_scan_sequence( + fn externally_triggered_scan_for_new_payables_is_prevented_if_automatic_scans_are_enabled( ) { - todo!("write me up") + todo!("write me up...new payables") + } + + #[test] + fn externally_triggered_scan_for_pending_payables_is_prevented_if_automatic_scans_are_enabled() { + todo!("write me up...pending payables") + } + + #[test] + fn externally_triggered_scan_for_receivables_is_prevented_if_automatic_scans_are_enabled() { + todo!("write me up...receivables") } #[test] @@ -2853,7 +2860,7 @@ mod tests { SystemTime::now(), None, &subject.logger, - false, + true, ); System::current().stop(); diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 164f97e33..1b531a84d 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -55,7 +55,7 @@ use crate::db_config::persistent_configuration::{PersistentConfiguration, Persis // Leave the individual scanner objects private! pub struct Scanners { payable: Box, - unresolved_pending_payable: bool, + aware_of_unresolved_pending_payable: bool, pending_payable: Box< dyn PrivateScanner< ScanForPendingPayables, @@ -109,7 +109,7 @@ impl Scanners { Scanners { payable, - unresolved_pending_payable: false, + aware_of_unresolved_pending_payable: false, pending_payable, receivable, } @@ -121,14 +121,10 @@ impl Scanners { timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - pending_payable_sequence_in_process: bool, + automatic_scans_enabled: bool, ) -> Result { let triggered_manually = response_skeleton_opt.is_some(); - // Under normal circumstances, it is guaranteed that the new-payable scanner will never - // overlap with the execution of the pending payable scanner, as they are safely - // and automatically scheduled to run sequentially. However, since we allow for unexpected, - // manually triggered scans, a conflict is possible and must be handled. - if triggered_manually && pending_payable_sequence_in_process { + if triggered_manually && automatic_scans_enabled { todo!() // ManualTriggerError } @@ -182,15 +178,16 @@ impl Scanners { timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - pending_payable_sequence_in_process: bool, + automatic_scans_enabled: bool, ) -> Result { let triggered_manually = response_skeleton_opt.is_some(); - if triggered_manually && pending_payable_sequence_in_process { - todo!() // ManualTriggerError - } - if triggered_manually && !self.unresolved_pending_payable { - todo!() //ManualTriggerError + if triggered_manually && automatic_scans_enabled { + todo!("forbidden") + } else if triggered_manually && !self.aware_of_unresolved_pending_payable { + todo!("useless") + } else if !self.aware_of_unresolved_pending_payable { + todo!("unreachable") } match ( @@ -198,12 +195,12 @@ impl Scanners { self.payable.scan_started_at(), ) { (Some(pp_timestamp), Some(p_timestamp)) => - // If you're wondering, then yes, this should be sacre in the relationship between - // Pending Payable and New Payable. + // If you're wondering, then yes, this should be the sacre truth between + // PendingPayableScanner and NewPayableScanner. { unreachable!( - "Both payable scanners should never be allowed to run in parallel. \ - Scan for pending payables started at: {}, scan for payables started at: {}", + "Both payable scanners should never be allowed to run in parallel. Scan for \ + pending payables started at: {}, scan for payables started at: {}", BeginScanError::timestamp_as_string(pp_timestamp), BeginScanError::timestamp_as_string(p_timestamp) ) @@ -250,7 +247,7 @@ impl Scanners { ) -> Option { let scan_result = self.payable.finish_scan(msg, logger); match scan_result.result { - OperationOutcome::NewPendingPayable => self.unresolved_pending_payable = true, + OperationOutcome::NewPendingPayable => self.aware_of_unresolved_pending_payable = true, OperationOutcome::Failure => (), }; scan_result.ui_response_opt @@ -273,7 +270,7 @@ impl Scanners { } pub fn scan_error_scan_reset(&self, error: &ScanError) { - todo!() + todo!("test me locally") } pub fn try_skipping_payable_adjustment( @@ -1789,7 +1786,7 @@ mod tests { &payment_thresholds ); assert_eq!(payable_scanner.common.initiated_at_opt.is_some(), false); - assert_eq!(scanners.unresolved_pending_payable, false); + assert_eq!(scanners.aware_of_unresolved_pending_payable, false); assert_eq!( pending_payable_scanner.when_pending_too_long_sec, when_pending_too_long_sec @@ -1851,7 +1848,7 @@ mod tests { now, None, &Logger::new(test_name), - false, + true, ); let timestamp = subject.payable.scan_started_at(); @@ -1893,7 +1890,7 @@ mod tests { previous_scan_started_at, None, &Logger::new("test"), - false, + true, ); let result = subject.start_new_payable_scan_guarded( @@ -1901,7 +1898,7 @@ mod tests { SystemTime::now(), None, &Logger::new("test"), - false, + true, ); let is_scan_running = subject.payable.scan_started_at().is_some(); @@ -2147,16 +2144,16 @@ mod tests { payable_scanner.mark_as_started(SystemTime::now()); let mut subject = make_dull_subject(); subject.payable = Box::new(payable_scanner); - let unresolved_pending_payable_before = subject.unresolved_pending_payable; + let aware_of_unresolved_pending_payable_before = subject.aware_of_unresolved_pending_payable; let node_to_ui_msg = subject.finish_payable_scan(sent_payable, &logger); let is_scan_running = subject.scan_started_at(ScanType::Payables).is_some(); - let unresolved_pending_payable_after = subject.unresolved_pending_payable; + let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; assert_eq!(node_to_ui_msg, None); assert_eq!(is_scan_running, false); - assert_eq!(unresolved_pending_payable_before, false); - assert_eq!(unresolved_pending_payable_after, true); + assert_eq!(aware_of_unresolved_pending_payable_before, false); + assert_eq!(aware_of_unresolved_pending_payable_after, true); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); assert_eq!( *fingerprints_rowids_params, @@ -2537,16 +2534,16 @@ mod tests { }; let mut subject = make_dull_subject(); subject.payable = Box::new(payable_scanner); - let unresolved_pending_payable_before = subject.unresolved_pending_payable; + let aware_of_unresolved_pending_payable_before = subject.aware_of_unresolved_pending_payable; let node_to_ui_msg_opt = subject.finish_payable_scan(sent_payable, &logger); - let unresolved_pending_payable_after = subject.unresolved_pending_payable; + let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; System::current().stop(); system.run(); assert_eq!(node_to_ui_msg_opt, None); - assert_eq!(unresolved_pending_payable_before, false); - assert_eq!(unresolved_pending_payable_after, false); + assert_eq!(aware_of_unresolved_pending_payable_before, false); + assert_eq!(aware_of_unresolved_pending_payable_after, false); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); assert_eq!( *fingerprints_rowids_params, @@ -2583,13 +2580,13 @@ mod tests { let payable_scanner = PayableScannerBuilder::new().build(); let mut subject = make_dull_subject(); subject.payable = Box::new(payable_scanner); - let unresolved_pending_payable_before = subject.unresolved_pending_payable; + let aware_of_unresolved_pending_payable_before = subject.aware_of_unresolved_pending_payable; subject.finish_payable_scan(sent_payable, &Logger::new(test_name)); - let unresolved_pending_payable_after = subject.unresolved_pending_payable; - assert_eq!(unresolved_pending_payable_before, false); - assert_eq!(unresolved_pending_payable_after, false); + let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; + assert_eq!(aware_of_unresolved_pending_payable_before, false); + assert_eq!(aware_of_unresolved_pending_payable_after, false); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing(&format!( "DEBUG: {test_name}: Got 0 properly sent payables of an unknown number of attempts" @@ -4332,7 +4329,7 @@ mod tests { fn make_dull_subject() -> Scanners { Scanners { payable: Box::new(NullScanner::new()), - unresolved_pending_payable: false, + aware_of_unresolved_pending_payable: false, pending_payable: Box::new(NullScanner::new()), receivable: Box::new(NullScanner::new()), } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 28983e12e..4830a94c7 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -19,7 +19,6 @@ pub struct ScanSchedulers { pub payable: PayableScanScheduler, pub pending_payable: SimplePeriodicalScanScheduler, pub receivable: SimplePeriodicalScanScheduler, - pub pending_payable_sequence_in_process: bool, pub automatic_scans_enabled: bool, } @@ -31,7 +30,6 @@ impl ScanSchedulers { scan_intervals.pending_payable_scan_interval, ), receivable: SimplePeriodicalScanScheduler::new(scan_intervals.receivable_scan_interval), - pending_payable_sequence_in_process: false, automatic_scans_enabled, } } @@ -312,7 +310,6 @@ mod tests { .borrow(), false ); - assert_eq!(schedulers.pending_payable_sequence_in_process, false); assert_eq!(schedulers.automatic_scans_enabled, true) } From 493df7bfa5a0d2250d67e7c14e7cb193b23f6d48 Mon Sep 17 00:00:00 2001 From: Bert Date: Wed, 14 May 2025 23:39:35 +0200 Subject: [PATCH 19/49] GH-602: Manual scanners prevented by the automation. Tested --- node/src/accountant/mod.rs | 131 +++++++++++++++++++++++----- node/src/accountant/scanners/mod.rs | 95 ++++++++++++++------ node/src/test_utils/recorder.rs | 4 +- 3 files changed, 180 insertions(+), 50 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 0601906d4..8b5327cf5 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -57,14 +57,14 @@ use itertools::Either; use itertools::Itertools; use masq_lib::crash_point::CrashPoint; use masq_lib::logger::Logger; -use masq_lib::messages::{ScanType, UiFinancialsResponse}; +use masq_lib::messages::{ScanType, UiFinancialsResponse, UiScanResponse}; use masq_lib::messages::{FromMessageBody, ToMessageBody, UiFinancialsRequest}; use masq_lib::messages::{ QueryResults, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, UiScanRequest, }; use masq_lib::ui_gateway::MessageTarget::ClientId; -use masq_lib::ui_gateway::{MessageBody, MessagePath}; +use masq_lib::ui_gateway::{MessageBody, MessagePath, MessageTarget}; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::any::type_name; @@ -903,7 +903,7 @@ impl Accountant { SystemTime::now(), response_skeleton_opt, &self.logger, - self.scan_schedulers.automatic_scans_enabled + self.scan_schedulers.automatic_scans_enabled, ), None => Err(BeginScanError::NoConsumingWalletFound), }; @@ -919,17 +919,27 @@ impl Accountant { // ScanScheduleHint::DoNotSchedule } Err(e) => { - e.handle_error( + e.log_error( &self.logger, ScanType::Payables, response_skeleton_opt.is_some(), ); - //TODO simplify after having been properly tested - if e == BeginScanError::NothingToProcess && response_skeleton_opt.is_none() { + response_skeleton_opt.map(|skeleton| { + self.ui_message_sub_opt + .as_ref() + .expect("UiGateway is unbound") + .try_send(NodeToUiMessage { + target: MessageTarget::ClientId(skeleton.client_id), + body: UiScanResponse {}.tmb(skeleton.context_id), + }) + .expect("UiGateway is dead"); + }); + + if e == BeginScanError::NothingToProcess { ScanScheduleHint::Schedule } else { - todo!("Just hit me up") + ScanScheduleHint::DoNotSchedule } } } @@ -958,12 +968,14 @@ impl Accountant { .try_send(scan_message) .expect("BlockchainBridge is dead"); } - Err(e) => todo!("the same test here pls!!!") - // e.handle_error( - // &self.logger, - // ScanType::Payables, - // response_skeleton_opt.is_some(), - //), + Err(e) => todo!("the same test here pls!!!"), // e.log_error( + // &self.logger, + // ScanType::Payables, + // response_skeleton_opt.is_some(), + //), + // response_skeleton_opt.map(|skeleton|{ + // self.ui_message_sub_opt.as_ref().expect("UiGateway is unbound").try_send(NodeToUiMessage{ target: MessageTarget::ClientId(skeleton.client_id), body: UiScanResponse{}.tmb(skeleton.context_id) }).expect("UiGateway is dead"); + // }); } } @@ -992,11 +1004,22 @@ impl Accountant { .expect("BlockchainBridge is dead"); } Err(e) => { - e.handle_error( + e.log_error( &self.logger, ScanType::PendingPayables, response_skeleton_opt.is_some(), ); + + response_skeleton_opt.map(|skeleton| { + self.ui_message_sub_opt + .as_ref() + .expect("UiGateway is unbound") + .try_send(NodeToUiMessage { + target: MessageTarget::ClientId(skeleton.client_id), + body: UiScanResponse {}.tmb(skeleton.context_id), + }) + .expect("UiGateway is dead"); + }); } } } @@ -1011,6 +1034,7 @@ impl Accountant { SystemTime::now(), response_skeleton_opt, &self.logger, + self.scan_schedulers.automatic_scans_enabled, ); match result { @@ -1021,11 +1045,22 @@ impl Accountant { .try_send(scan_message) .expect("BlockchainBridge is dead"), Err(e) => { - e.handle_error( + e.log_error( &self.logger, ScanType::Receivables, response_skeleton_opt.is_some(), ); + + response_skeleton_opt.map(|skeleton| { + self.ui_message_sub_opt + .as_ref() + .expect("UiGateway is unbound") + .try_send(NodeToUiMessage { + target: MessageTarget::ClientId(skeleton.client_id), + body: UiScanResponse {}.tmb(skeleton.context_id), + }) + .expect("UiGateway is dead"); + }); } } } @@ -1182,7 +1217,7 @@ mod tests { use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::neighborhood::ConfigChange; use crate::sub_lib::neighborhood::{Hops, WalletPair}; - use crate::test_utils::recorder::{make_recorder, SetUpCounterMsgs}; + use crate::test_utils::recorder::{make_recorder, PeerActorsBuilder, SetUpCounterMsgs}; use crate::test_utils::recorder::peer_actors_builder; use crate::test_utils::recorder::Recorder; use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; @@ -1812,10 +1847,12 @@ mod tests { .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let subject_addr = subject.start(); let system = System::new(test_name); let peer_actors = peer_actors_builder() .blockchain_bridge(blockchain_bridge) + .ui_gateway(ui_gateway) .build(); let first_message = NodeFromUiMessage { client_id: 1234, @@ -1838,22 +1875,72 @@ mod tests { test_name )); assert_eq!(blockchain_bridge_recording.len(), 1); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let msg = ui_gateway_recording.get_record::(0); + assert_eq!(msg.body, UiScanResponse {}.tmb(4321)); } - #[test] - fn externally_triggered_scan_for_new_payables_is_prevented_if_automatic_scans_are_enabled( + fn test_externally_triggered_scan_is_prevented_if_automatic_scans_are_enabled( + test_name: &str, + scan_type: ScanType, ) { - todo!("write me up...new payables") + init_test_logging(); + let (blockchain_bridge, _, blockchain_bridge_recorder_arc) = make_recorder(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .consuming_wallet(make_wallet("abc")) + .build(); + let subject_addr = subject.start(); + let system = System::new(test_name); + let peer_actors = PeerActorsBuilder::default() + .ui_gateway(ui_gateway) + .blockchain_bridge(blockchain_bridge) + .build(); + let ui_message = NodeFromUiMessage { + client_id: 1234, + body: UiScanRequest { scan_type }.tmb(6789), + }; + subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + + subject_addr.try_send(ui_message).unwrap(); + + assert_eq!(system.run(), 0); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let msg = ui_gateway_recording.get_record::(0); + assert_eq!(msg.body, UiScanResponse {}.tmb(6789)); + assert_eq!(ui_gateway_recording.len(), 1); + let blockchain_bridge_recorder = blockchain_bridge_recorder_arc.lock().unwrap(); + assert_eq!(blockchain_bridge_recorder.len(), 0); + TestLogHandler::new().exists_log_containing( + format!( + "WARN: {test_name}: Manual {:?} scan \ + was denied. Automatic scanning setup prevents manual triggers.", + scan_type + ) + .as_str(), + ); } #[test] - fn externally_triggered_scan_for_pending_payables_is_prevented_if_automatic_scans_are_enabled() { - todo!("write me up...pending payables") + fn externally_triggered_scan_for_new_payables_is_prevented_if_automatic_scans_are_enabled() { + test_externally_triggered_scan_is_prevented_if_automatic_scans_are_enabled("externally_triggered_scan_for_new_payables_is_prevented_if_automatic_scans_are_enabled", ScanType::Payables) + } + + #[test] + fn externally_triggered_scan_for_pending_payables_is_prevented_if_automatic_scans_are_enabled() + { + test_externally_triggered_scan_is_prevented_if_automatic_scans_are_enabled("externally_triggered_scan_for_pending_payables_is_prevented_if_automatic_scans_are_enabled", ScanType::PendingPayables) } #[test] fn externally_triggered_scan_for_receivables_is_prevented_if_automatic_scans_are_enabled() { - todo!("write me up...receivables") + test_externally_triggered_scan_is_prevented_if_automatic_scans_are_enabled( + "externally_triggered_scan_for_receivables_is_prevented_if_automatic_scans_are_enabled", + ScanType::Receivables, + ) } #[test] diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 94b2b7744..48e4477e8 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -28,7 +28,7 @@ use crate::sub_lib::accountant::{ }; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::wallet::Wallet; -use actix::{Message}; +use actix::{Message, Recipient}; use itertools::{Either, Itertools}; use masq_lib::logger::Logger; use masq_lib::logger::TIME_FORMATTING_STRING; @@ -125,7 +125,9 @@ impl Scanners { ) -> Result { let triggered_manually = response_skeleton_opt.is_some(); if triggered_manually && automatic_scans_enabled { - todo!() // ManualTriggerError + return Err(BeginScanError::ManualTriggerError( + MTError::AutomaticScanConflict, + )); } if let Some(started_at) = self.payable.scan_started_at() { @@ -182,8 +184,10 @@ impl Scanners { ) -> Result { let triggered_manually = response_skeleton_opt.is_some(); - if triggered_manually && automatic_scans_enabled { - todo!("forbidden") + if triggered_manually && automatic_scans_enabled { + return Err(BeginScanError::ManualTriggerError( + MTError::AutomaticScanConflict, + )); } else if triggered_manually && !self.aware_of_unresolved_pending_payable { todo!("useless") } else if !self.aware_of_unresolved_pending_payable { @@ -229,7 +233,15 @@ impl Scanners { timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, + automatic_scans_enabled: bool, ) -> Result { + let triggered_manually = response_skeleton_opt.is_some(); + + if triggered_manually && automatic_scans_enabled { + return Err(BeginScanError::ManualTriggerError( + MTError::AutomaticScanConflict, + )); + } if let Some(started_at) = self.receivable.scan_started_at() { return Err(BeginScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Receivables, @@ -1250,43 +1262,58 @@ pub enum BeginScanError { started_at: SystemTime, }, CalledFromNullScanner, // Exclusive for tests - ManualTriggerError(String), + ManualTriggerError(MTError), } impl BeginScanError { - pub fn handle_error( - &self, - logger: &Logger, - scan_type: ScanType, - is_externally_triggered: bool, - ) { - let log_message_opt = match self { - BeginScanError::NothingToProcess => Some(format!( + pub fn log_error(&self, logger: &Logger, scan_type: ScanType, is_externally_triggered: bool) { + enum ErrorType { + Temporary(String), + Permanent(String), + } + + let log_message = match self { + BeginScanError::NothingToProcess => ErrorType::Temporary(format!( "There was nothing to process during {:?} scan.", scan_type )), BeginScanError::ScanAlreadyRunning { pertinent_scanner, started_at, - } => Some(Self::scan_already_running_msg( + } => ErrorType::Temporary(Self::scan_already_running_msg( *pertinent_scanner, *started_at, )), - BeginScanError::NoConsumingWalletFound => Some(format!( + BeginScanError::NoConsumingWalletFound => ErrorType::Permanent(format!( "Cannot initiate {:?} scan because no consuming wallet was found.", scan_type )), BeginScanError::CalledFromNullScanner => match cfg!(test) { - true => None, + true => todo!(), //None, false => panic!("Null Scanner shouldn't be running inside production code."), }, - BeginScanError::ManualTriggerError(msg) => todo!(), + BeginScanError::ManualTriggerError(e) => ErrorType::Permanent(format!( + "Manual {:?} scan was denied. {}", + scan_type, + match e { + MTError::AutomaticScanConflict => + "Automatic scanning setup prevents manual triggers.", + MTError::PointlessRequest => todo!(), + } + )), }; - if let Some(log_message) = log_message_opt { - match is_externally_triggered { - true => info!(logger, "{}", log_message), - false => debug!(logger, "{}", log_message), + match is_externally_triggered { + true => match log_message { + ErrorType::Temporary(msg) => info!(logger, "{}", msg), + ErrorType::Permanent(msg) => warning!(logger, "{}", msg), + }, + + false => { + match log_message { + ErrorType::Temporary(msg) => todo!(), //debug!(logger, "{}", log_message), + ErrorType::Permanent(msg) => todo!(), + } } } } @@ -1313,6 +1340,12 @@ impl BeginScanError { } } +#[derive(Debug, PartialEq, Eq)] +pub enum MTError { + AutomaticScanConflict, + PointlessRequest, +} + // Note that this location was chosen because the following mocks need to implement a private trait // from this file #[cfg(test)] @@ -2144,7 +2177,8 @@ mod tests { payable_scanner.mark_as_started(SystemTime::now()); let mut subject = make_dull_subject(); subject.payable = Box::new(payable_scanner); - let aware_of_unresolved_pending_payable_before = subject.aware_of_unresolved_pending_payable; + let aware_of_unresolved_pending_payable_before = + subject.aware_of_unresolved_pending_payable; let node_to_ui_msg = subject.finish_payable_scan(sent_payable, &logger); @@ -2534,7 +2568,8 @@ mod tests { }; let mut subject = make_dull_subject(); subject.payable = Box::new(payable_scanner); - let aware_of_unresolved_pending_payable_before = subject.aware_of_unresolved_pending_payable; + let aware_of_unresolved_pending_payable_before = + subject.aware_of_unresolved_pending_payable; let node_to_ui_msg_opt = subject.finish_payable_scan(sent_payable, &logger); @@ -2580,7 +2615,8 @@ mod tests { let payable_scanner = PayableScannerBuilder::new().build(); let mut subject = make_dull_subject(); subject.payable = Box::new(payable_scanner); - let aware_of_unresolved_pending_payable_before = subject.aware_of_unresolved_pending_payable; + let aware_of_unresolved_pending_payable_before = + subject.aware_of_unresolved_pending_payable; subject.finish_payable_scan(sent_payable, &Logger::new(test_name)); @@ -3826,6 +3862,7 @@ mod tests { now, None, &Logger::new(test_name), + true, ); let is_scan_running = subject.receivable.scan_started_at().is_some(); @@ -3854,14 +3891,20 @@ mod tests { .receivable_dao(receivable_dao) .build(); subject.receivable = Box::new(receivable_scanner); - let _ = - subject.start_receivable_scan_guarded(&earning_wallet, now, None, &Logger::new("test")); + let _ = subject.start_receivable_scan_guarded( + &earning_wallet, + now, + None, + &Logger::new("test"), + true, + ); let result = subject.start_receivable_scan_guarded( &earning_wallet, SystemTime::now(), None, &Logger::new("test"), + true, ); let is_scan_running = subject.receivable.scan_started_at().is_some(); diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index af9a1e73d..509529780 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -300,7 +300,7 @@ impl Recorder { { let counter_msg_opt = self.check_on_counter_msg(&msg); - let kill_system = if let Some(stop_conditions) = &mut self.stop_conditions_opt { + let stop_system = if let Some(stop_conditions) = &mut self.stop_conditions_opt { stop_conditions.resolve_stop_conditions::(&msg) } else { false @@ -312,7 +312,7 @@ impl Recorder { sendable_msgs.into_iter().for_each(|msg| msg.try_send()) } - if kill_system { + if stop_system { System::current().stop() } } From bc311ab645b65540b7572cd9367fbae5898d8718 Mon Sep 17 00:00:00 2001 From: Bert Date: Thu, 15 May 2025 12:44:54 +0200 Subject: [PATCH 20/49] GH-602: More tests written; mainly constraining the manual scanning --- node/src/accountant/mod.rs | 52 +++++++++--- node/src/accountant/scanners/mod.rs | 118 ++++++++++++++++++++-------- 2 files changed, 128 insertions(+), 42 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 8b5327cf5..9073480b2 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1883,16 +1883,43 @@ mod tests { fn test_externally_triggered_scan_is_prevented_if_automatic_scans_are_enabled( test_name: &str, scan_type: ScanType, + ) { + let expected_log_msg = format!( + "WARN: {test_name}: Manual {:?} scan was denied. Automatic scanning setup prevents \ + manual triggers.", + scan_type + ); + + test_externally_triggered_scan_is_prevented_if( + true, + true, + test_name, + scan_type, + &expected_log_msg, + ) + } + + fn test_externally_triggered_scan_is_prevented_if( + automatic_scans_enabled: bool, + aware_of_unresolved_pending_payables: bool, + test_name: &str, + scan_type: ScanType, + expected_log_message: &str, ) { init_test_logging(); let (blockchain_bridge, _, blockchain_bridge_recorder_arc) = make_recorder(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let ui_gateway = ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); - let subject = AccountantBuilder::default() + let mut subject = AccountantBuilder::default() .logger(Logger::new(test_name)) .consuming_wallet(make_wallet("abc")) .build(); + subject.scan_schedulers.automatic_scans_enabled = automatic_scans_enabled; + subject + .scanners + .set_aware_of_unresolved_pending_payables(aware_of_unresolved_pending_payables); + subject.scanners.set_initial_scan(false); let subject_addr = subject.start(); let system = System::new(test_name); let peer_actors = PeerActorsBuilder::default() @@ -1914,14 +1941,7 @@ mod tests { assert_eq!(ui_gateway_recording.len(), 1); let blockchain_bridge_recorder = blockchain_bridge_recorder_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recorder.len(), 0); - TestLogHandler::new().exists_log_containing( - format!( - "WARN: {test_name}: Manual {:?} scan \ - was denied. Automatic scanning setup prevents manual triggers.", - scan_type - ) - .as_str(), - ); + TestLogHandler::new().exists_log_containing(expected_log_message); } #[test] @@ -1946,7 +1966,19 @@ mod tests { #[test] fn externally_triggered_scan_for_pending_payables_is_prevented_if_all_payments_already_complete( ) { - todo!("write me up") + let test_name = "externally_triggered_scan_for_pending_payables_is_prevented_if_all_payments_already_complete"; + let expected_log_msg = format!( + "INFO: {test_name}: Manual PendingPayables scan was \ + denied for a predictable zero effect. Run the Payable scanner first." + ); + + test_externally_triggered_scan_is_prevented_if( + false, + false, + test_name, + ScanType::PendingPayables, + &expected_log_msg, + ) } #[test] diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 48e4477e8..9b5633e61 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -56,6 +56,7 @@ use crate::db_config::persistent_configuration::{PersistentConfiguration, Persis pub struct Scanners { payable: Box, aware_of_unresolved_pending_payable: bool, + initial_scan: bool, pending_payable: Box< dyn PrivateScanner< ScanForPendingPayables, @@ -110,6 +111,7 @@ impl Scanners { Scanners { payable, aware_of_unresolved_pending_payable: false, + initial_scan: true, pending_payable, receivable, } @@ -183,17 +185,10 @@ impl Scanners { automatic_scans_enabled: bool, ) -> Result { let triggered_manually = response_skeleton_opt.is_some(); - - if triggered_manually && automatic_scans_enabled { - return Err(BeginScanError::ManualTriggerError( - MTError::AutomaticScanConflict, - )); - } else if triggered_manually && !self.aware_of_unresolved_pending_payable { - todo!("useless") - } else if !self.aware_of_unresolved_pending_payable { - todo!("unreachable") - } - + self.check_general_conditions_for_pending_payable_scan( + triggered_manually, + automatic_scans_enabled, + )?; match ( self.pending_payable.scan_started_at(), self.payable.scan_started_at(), @@ -318,6 +313,33 @@ impl Scanners { QualifiedPayablesMessage, >>::start_scan(scanner, wallet, timestamp, response_skeleton_opt, logger) } + + fn check_general_conditions_for_pending_payable_scan( + &mut self, + triggered_manually: bool, + automatic_scans_enabled: bool, + ) -> Result<(), BeginScanError> { + if triggered_manually && automatic_scans_enabled { + Err(BeginScanError::ManualTriggerError( + MTError::AutomaticScanConflict, + )) + } else if self.initial_scan { + todo!("other conditions forgiven") + } else if triggered_manually && !self.aware_of_unresolved_pending_payable { + Err(BeginScanError::ManualTriggerError( + MTError::UnnecessaryRequest { + hint_opt: Some("Run the Payable scanner first.".to_string()), + }, + )) + } else if !self.aware_of_unresolved_pending_payable { + unreachable!( + "Automatic pending payable scan should never start if there are no pending \ + payables to process." + ) + } else { + todo!() + } + } } trait PrivateScanner: @@ -1292,15 +1314,21 @@ impl BeginScanError { true => todo!(), //None, false => panic!("Null Scanner shouldn't be running inside production code."), }, - BeginScanError::ManualTriggerError(e) => ErrorType::Permanent(format!( - "Manual {:?} scan was denied. {}", - scan_type, - match e { - MTError::AutomaticScanConflict => - "Automatic scanning setup prevents manual triggers.", - MTError::PointlessRequest => todo!(), - } - )), + BeginScanError::ManualTriggerError(e) => match e { + MTError::AutomaticScanConflict => ErrorType::Permanent(format!( + "Manual {:?} scan was denied. Automatic scanning setup prevents manual \ + triggers.", + scan_type + )), + MTError::UnnecessaryRequest { hint_opt } => ErrorType::Temporary(format!( + "Manual {:?} scan was denied for a predictable zero effect.{}", + scan_type, + match hint_opt { + Some(hint) => format!(" {}", hint), + None => todo!(), + } + )), + }, }; match is_externally_triggered { @@ -1343,7 +1371,7 @@ impl BeginScanError { #[derive(Debug, PartialEq, Eq)] pub enum MTError { AutomaticScanConflict, - PointlessRequest, + UnnecessaryRequest { hint_opt: Option }, } // Note that this location was chosen because the following mocks need to implement a private trait @@ -1739,6 +1767,14 @@ mod tests { } } + pub fn set_aware_of_unresolved_pending_payables(&mut self, value: bool) { + self.aware_of_unresolved_pending_payable = value + } + + pub fn set_initial_scan(&mut self, value: bool) { + self.initial_scan = value + } + fn simple_scanner_timestamp_treatment( scanner: &mut Scanner, value: MarkScanner, @@ -1820,6 +1856,7 @@ mod tests { ); assert_eq!(payable_scanner.common.initiated_at_opt.is_some(), false); assert_eq!(scanners.aware_of_unresolved_pending_payable, false); + assert_eq!(scanners.initial_scan, true); assert_eq!( pending_payable_scanner.when_pending_too_long_sec, when_pending_too_long_sec @@ -3045,7 +3082,7 @@ mod tests { now, None, &Logger::new(test_name), - false, + true, ); let no_of_pending_payables = fingerprints.len(); @@ -3080,20 +3117,15 @@ mod tests { let payable_scanner = PayableScannerBuilder::new().build(); subject.payable = Box::new(payable_scanner); let logger = Logger::new("test"); - let _ = subject.start_pending_payable_scan_guarded( - &consuming_wallet, - now, - None, - &logger, - false, - ); + let _ = + subject.start_pending_payable_scan_guarded(&consuming_wallet, now, None, &logger, true); let result = subject.start_pending_payable_scan_guarded( &consuming_wallet, SystemTime::now(), None, &logger, - false, + true, ); let is_scan_running = subject.pending_payable.scan_started_at().is_some(); @@ -3124,7 +3156,7 @@ mod tests { SystemTime::now(), None, &logger, - false, + true, ); let is_scan_running = subject.pending_payable.scan_started_at().is_some(); @@ -3163,7 +3195,7 @@ mod tests { SystemTime::now(), None, &Logger::new("test"), - false, + true, ); })) .unwrap_err(); @@ -3201,6 +3233,27 @@ mod tests { ) } + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: Automatic pending payable \ + scan should never start if there are no pending payables to process." + )] + fn pending_payable_scanner_bumps_into_zero_pending_payable_awareness_in_the_automatic_mode() { + let consuming_wallet = make_paying_wallet(b"consuming"); + let mut subject = make_dull_subject(); + let mut pending_payable_scanner = PendingPayableScannerBuilder::new().build(); + subject.pending_payable = Box::new(pending_payable_scanner); + subject.aware_of_unresolved_pending_payable = false; + + let _ = subject.start_pending_payable_scan_guarded( + &consuming_wallet, + SystemTime::now(), + None, + &Logger::new("test"), + true, + ); + } + #[test] fn pending_payable_scanner_throws_an_error_when_no_fingerprint_is_found() { let now = SystemTime::now(); @@ -4373,6 +4426,7 @@ mod tests { Scanners { payable: Box::new(NullScanner::new()), aware_of_unresolved_pending_payable: false, + initial_scan: false, pending_payable: Box::new(NullScanner::new()), receivable: Box::new(NullScanner::new()), } From f1169b679d59d3188391350fd2cd4dbad618a2bc Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 18 May 2025 21:51:01 +0200 Subject: [PATCH 21/49] GH-602: before trying to implement the ScanScheduleHintErrorResolver --- node/src/accountant/mod.rs | 428 ++++++++++++++---- node/src/accountant/scanners/mod.rs | 160 ++++--- .../accountant/scanners/scan_schedulers.rs | 257 +---------- node/src/accountant/scanners/test_utils.rs | 2 +- node/src/sub_lib/peer_actors.rs | 2 + node/src/test_utils/recorder.rs | 61 ++- 6 files changed, 502 insertions(+), 408 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 9073480b2..43395d7f7 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -25,7 +25,7 @@ use crate::accountant::financials::visibility_restricted_module::{ use crate::accountant::scanners::payable_scanner_extension::msgs::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; -use crate::accountant::scanners::{BeginScanError, Scanners}; +use crate::accountant::scanners::{StartScanError, Scanners}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, PendingPayableFingerprintSeeds, RetrieveTransactions}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; @@ -74,9 +74,10 @@ use std::fmt::Display; use std::ops::{Div, Mul}; use std::path::Path; use std::rc::Rc; +use std::str::RMatches; use std::time::SystemTime; use web3::types::H256; -use crate::accountant::scanners::scan_schedulers::{AutomaticSchedulingAwareScanner, ScanScheduleHint, ScanSchedulers}; +use crate::accountant::scanners::scan_schedulers::{ScanScheduleHint, ScanSchedulers}; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; @@ -224,7 +225,14 @@ impl Handler for Accountant { // the RetryPayableScanner, which finishes, and the PendingPayablesScanner is scheduled // to run again. Therefore, not from here. let response_skeleton_opt = msg.response_skeleton_opt; - self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) + if self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) + == ScanScheduleHint::Schedule + { + todo!() + // self.scan_schedulers + // .pending_payable + // .schedule(ctx, response_skeleton_opt); + } } } @@ -241,9 +249,7 @@ impl Handler for Accountant { if self.handle_request_of_scan_for_new_payable(response_skeleton) == ScanScheduleHint::Schedule { - self.scan_schedulers - .payable - .schedule_for_new_payable(ctx, None) + self.scan_schedulers.payable.schedule_for_new_payable(ctx) } } } @@ -266,7 +272,7 @@ impl Handler for Accountant { // By this time we know it is an automatic scanner, which is always rescheduled right away, // no matter what its outcome is. self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); - todo!("receivable scanner periodical") + self.scan_schedulers.receivable.schedule(ctx); } } @@ -288,9 +294,7 @@ impl Handler for Accountant { // would involve payables with fresh nonces. The job is done. } else { todo!("Finishing PendingPayable scan. Automatic scanning."); - self.scan_schedulers - .payable - .schedule_for_new_payable(ctx, response_skeleton_opt) + self.scan_schedulers.payable.schedule_for_new_payable(ctx) } } PendingPayableScanResult::PaymentRetryRequired => self @@ -896,7 +900,7 @@ impl Accountant { &mut self, response_skeleton_opt: Option, ) -> ScanScheduleHint { - let result: Result = + let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( consuming_wallet, @@ -905,7 +909,7 @@ impl Accountant { &self.logger, self.scan_schedulers.automatic_scans_enabled, ), - None => Err(BeginScanError::NoConsumingWalletFound), + None => Err(StartScanError::NoConsumingWalletFound), }; match result { @@ -936,8 +940,15 @@ impl Accountant { .expect("UiGateway is dead"); }); - if e == BeginScanError::NothingToProcess { + // TODO is this comment correct? + // At scanners with scheduling usually happening at their ultimate end, we must be + // thorough with evaluating errors because an inadequate reaction could disrupt + // the whole scan chain. Scheduling must often be done despite the error. + if e == StartScanError::NothingToProcess { + todo!("Shouldn't we schedule with every error????"); ScanScheduleHint::Schedule + } else if e == StartScanError::NoConsumingWalletFound { + todo!("schedule") } else { ScanScheduleHint::DoNotSchedule } @@ -948,8 +959,8 @@ impl Accountant { fn handle_request_of_scan_for_retry_payable( &mut self, response_skeleton_opt: Option, - ) { - let result: Result = + ) -> ScanScheduleHint { + let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_retry_payable_scan_guarded( consuming_wallet, @@ -957,7 +968,7 @@ impl Accountant { response_skeleton_opt, &self.logger, ), - None => todo!("I need a test here"), //Err(BeginScanError::NoConsumingWalletFound), + None => todo!("I need a test here"), //Err(StartScanError::NoConsumingWalletFound), }; match result { @@ -967,23 +978,47 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); + todo!() + } + Err(e) => { + todo!("process...and eventually schedule for pending payable"); + // e.log_error( + // &self.logger, + // ScanType::Payables, + // response_skeleton_opt.is_some(), + // ); + // + // response_skeleton_opt.map(|skeleton| { + // self.ui_message_sub_opt + // .as_ref() + // .expect("UiGateway is unbound") + // .try_send(NodeToUiMessage { + // target: MessageTarget::ClientId(skeleton.client_id), + // body: UiScanResponse {}.tmb(skeleton.context_id), + // }) + // .expect("UiGateway is dead"); + // }); + + // TODO is this comment correct? + // At scanners with scheduling usually happening at their ultimate end, we must be + // thorough with evaluating errors because an inadequate reaction could disrupt + // the whole scan chain. Scheduling must often be done despite the error. + if e == StartScanError::NothingToProcess { + todo!("unreachable"); + } else if response_skeleton_opt.is_some() { + todo!("do not schedule") + } else { + ScanScheduleHint::Schedule + } } - Err(e) => todo!("the same test here pls!!!"), // e.log_error( - // &self.logger, - // ScanType::Payables, - // response_skeleton_opt.is_some(), - //), - // response_skeleton_opt.map(|skeleton|{ - // self.ui_message_sub_opt.as_ref().expect("UiGateway is unbound").try_send(NodeToUiMessage{ target: MessageTarget::ClientId(skeleton.client_id), body: UiScanResponse{}.tmb(skeleton.context_id) }).expect("UiGateway is dead"); - // }); } } fn handle_request_of_scan_for_pending_payable( &mut self, response_skeleton_opt: Option, - ) { - let result: Result = + ) -> ScanScheduleHint { + let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_pending_payable_scan_guarded( consuming_wallet, // This argument is not used and is therefore irrelevant @@ -992,16 +1027,17 @@ impl Accountant { &self.logger, self.scan_schedulers.automatic_scans_enabled, ), - None => Err(BeginScanError::NoConsumingWalletFound), + None => Err(StartScanError::NoConsumingWalletFound), }; - match result { + let hint: ScanScheduleHint = match result { Ok(scan_message) => { self.request_transaction_receipts_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); + todo!("give a hint") } Err(e) => { e.log_error( @@ -1020,15 +1056,62 @@ impl Accountant { }) .expect("UiGateway is dead"); }); + // // TODO is this comment correct? + // // At scanners with scheduling usually happening at their ultimate end, we must be + // // thorough with evaluating errors because an inadequate reaction could disrupt + // // the whole scan chain. Scheduling must often be done despite the error. + // if response_skeleton_opt.is_some() { + // todo!("do not schedule") + // } + // + // if e == StartScanError::NothingToProcess { + // if !self.scanners.initial_pending_payable_scan() { + // todo!("unreachable") + // } + // } + // + // if matches!(e, StartScanError::ScanAlreadyRunning{..}) { + // todo!("nope") + // } + // + // // StartScanError::ManualTriggerError should be included in the condition for + // // externally triggered scans as this error implies only a single way it can occur. + // + // todo!("schedule for the rest")//ScanScheduleHint::Schedule + + // TODO is this comment correct? + // At scanners with scheduling usually happening at their ultimate end, we must be + // thorough with evaluating errors because an inadequate reaction could disrupt + // the whole scan chain. Scheduling must often be done despite the error. + if e == StartScanError::NothingToProcess && response_skeleton_opt.is_none() { + if self.scanners.initial_pending_payable_scan() { + todo!("schedule") + } else { + todo!("unreachable") + } + } else if e == StartScanError::NothingToProcess { + todo!("unreachable") + } else if e == StartScanError::NoConsumingWalletFound { + todo!("schedule") + } else { + todo!("do not schedule") + } } + }; + + if self.scanners.initial_pending_payable_scan() { + todo!("set me right") + //self.scanners.unset_initial_pending_payable_scan() } + + hint } fn handle_request_of_scan_for_receivable( &mut self, response_skeleton_opt: Option, ) { - let result: Result = + let result: Result = self.scanners.start_receivable_scan_guarded( &self.earning_wallet, SystemTime::now(), @@ -1065,6 +1148,15 @@ impl Accountant { } } + fn derive_scan_schedule_hint_from_error( + &self, + scan_type: ScanType, + e: StartScanError, + response_skeleton_opt: Option, + ) -> ScanScheduleHint { + todo!() + } + fn handle_externally_triggered_scan( &mut self, _ctx: &mut Context, @@ -1194,11 +1286,11 @@ mod tests { use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; use crate::accountant::scanners::test_utils::{MarkScanner, NewPayableScanDynIntervalComputerMock, ReplacementType, ScannerReplacement}; - use crate::accountant::scanners::{BeginScanError, ReceivableScanner}; + use crate::accountant::scanners::{StartScanError, ReceivableScanner, PendingPayableScanner}; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; - use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock}; + use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock, DaoWithDestination}; use crate::accountant::test_utils::{AccountantBuilder, BannedDaoMock}; use crate::accountant::Accountant; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; @@ -1251,8 +1343,9 @@ mod tests { use std::ops::{Sub}; use std::sync::Arc; use std::sync::Mutex; - use std::time::Duration; + use std::time::{Duration, UNIX_EPOCH}; use std::vec; + use actix::fut::Either::B; use nix::sys::socket::sockopt::BindToDevice; use crate::accountant::scanners::local_test_utils::{NullScanner, ScannerMock}; use crate::accountant::scanners::scan_schedulers::{NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal}; @@ -1919,7 +2012,7 @@ mod tests { subject .scanners .set_aware_of_unresolved_pending_payables(aware_of_unresolved_pending_payables); - subject.scanners.set_initial_scan(false); + subject.scanners.unset_initial_pending_payable_scan(); let subject_addr = subject.start(); let system = System::new(test_name); let peer_actors = PeerActorsBuilder::default() @@ -2102,12 +2195,12 @@ mod tests { ); subject.scan_schedulers.payable.dyn_interval_computer = Box::new( NewPayableScanDynIntervalComputerMock::default() - .compute_interval_results(Some(Duration::from_secs(500))), + .compute_interval_result(Some(Duration::from_secs(500))), ); let payable_scanner = ScannerMock::default() .scan_started_at_result(None) .scan_started_at_result(None) - .start_scan_result(Err(BeginScanError::NothingToProcess)); + .start_scan_result(Err(StartScanError::NothingToProcess)); subject .scanners .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( @@ -2197,7 +2290,7 @@ mod tests { system.run(); let after = SystemTime::now(); let mut start_scan_params = start_scan_params_arc.lock().unwrap(); - let (actual_wallet, actual_now, actual_response_skeleton_opt, actual_logger) = + let (actual_wallet, actual_now, actual_response_skeleton_opt, actual_logger, _) = start_scan_params.remove(0); assert_eq!(actual_wallet, consuming_wallet); assert_eq!(actual_response_skeleton_opt, response_skeleton_opt); @@ -2422,91 +2515,182 @@ mod tests { #[test] fn accountant_scans_after_startup() { - todo!("fix me....it should be like \ - 1) scan for pending payables....and when it ends, it should call the scan for new payables afterward, automatically.\ - 2) scan for receivables"); - // Note: We don't assert on the payable scanner this time because it follows the pending - // payable scanner and is beyond the scope of this test + // We do ensure the PendingPayableScanner runs before the NewPayableScanner. It does not + // matter a lot when does the ReceivableScanner take place. init_test_logging(); - let pending_payable_params_arc = Arc::new(Mutex::new(vec![])); - let new_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); - let paid_delinquencies_params_arc = Arc::new(Mutex::new(vec![])); - let scan_for_new_payables_notify_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); + let receivable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); + let payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); + let scan_for_pending_payables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let scan_for_new_payables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let scan_for_new_payables_notify_params_arc = Arc::new(Mutex::new(vec![])); + let scan_for_retry_payables_notify_params_arc = Arc::new(Mutex::new(vec![])); + let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); let scan_for_receivables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); - let (blockchain_bridge, _, _) = make_recorder(); let earning_wallet = make_wallet("earning"); + let consuming_wallet = make_wallet("consuming"); let system = System::new("accountant_scans_after_startup"); - let config = bc_from_wallets(make_wallet("buy"), earning_wallet.clone()); - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_params(&pending_payable_params_arc) - .return_all_errorless_fingerprints_result(vec![]); - let receivable_dao = ReceivableDaoMock::new() - .new_delinquencies_parameters(&new_delinquencies_params_arc) - .new_delinquencies_result(vec![]) - .paid_delinquencies_parameters(&paid_delinquencies_params_arc) - .paid_delinquencies_result(vec![]); + let _ = SystemKillerActor::new(Duration::from_secs(10)).start(); + let config = bc_from_wallets(consuming_wallet.clone(), earning_wallet.clone()); + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&pending_payable_start_scan_params_arc) + .start_scan_result(Err(StartScanError::NothingToProcess)); + let receivable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&receivable_start_scan_params_arc) + .start_scan_result(Err(StartScanError::NothingToProcess)); + let payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&payable_start_scan_params_arc) + .start_scan_result(Err(StartScanError::NothingToProcess)); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) - .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Mock( + receivable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&scan_for_pending_payables_notify_later_params_arc), + ); + let new_payable_expected_computed_interval = Duration::from_secs(3600); subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default() .notify_later_params(&scan_for_new_payables_notify_later_params_arc), ); + subject.scan_schedulers.payable.retry_payable_notify = Box::new( + NotifyHandleMock::default().notify_params(&scan_for_retry_payables_notify_params_arc), + ); subject.scan_schedulers.payable.new_payable_notify = Box::new( NotifyHandleMock::default().notify_params(&scan_for_new_payables_notify_params_arc), ); subject.scan_schedulers.receivable.handle = Box::new( NotifyLaterHandleMock::default() - .notify_later_params(&scan_for_receivables_notify_later_params_arc), + .notify_later_params(&scan_for_receivables_notify_later_params_arc) + .stop_system_on_count_received(1), ); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); + let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() + .compute_interval_params(&compute_interval_params_arc) + .compute_interval_result(Some(new_payable_expected_computed_interval)); + subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); + let peer_actors = peer_actors_builder().build(); let subject_addr: Addr = subject.start(); let subject_subs = Accountant::make_subs_from(&subject_addr); send_bind_message!(subject_subs, peer_actors); send_start_message!(subject_subs); - System::current().stop(); + // The system is stopped by the NotifyLaterHandleMock for the Receivable scanner system.run(); - let pending_payable_params = pending_payable_params_arc.lock().unwrap(); - // Proof of calling pieces of scan_for_delinquencies() - let mut new_delinquencies_params = new_delinquencies_params_arc.lock().unwrap(); - let (captured_timestamp, captured_curves) = new_delinquencies_params.remove(0); - let paid_delinquencies_params = paid_delinquencies_params_arc.lock().unwrap(); - assert_eq!(*pending_payable_params, vec![()]); - assert!(new_delinquencies_params.is_empty()); + // Assertions on the pending payable scanner proper functioning + let mut pending_payable_params = pending_payable_start_scan_params_arc.lock().unwrap(); + let ( + pp_wallet, + pp_scan_started_at, + pp_response_skeleton_opt, + pp_logger, + pp_trigger_msg_type_str, + ) = pending_payable_params.remove(0); + assert_eq!(pp_wallet, consuming_wallet); + assert_eq!(pp_response_skeleton_opt, None); assert!( - captured_timestamp < SystemTime::now() - && captured_timestamp >= from_time_t(to_time_t(SystemTime::now()) - 5) + pp_trigger_msg_type_str.contains("PendingPayable"), + "Should contain PendingPayable but {}", + pp_trigger_msg_type_str ); - assert_eq!(captured_curves, PaymentThresholds::default()); - assert_eq!(paid_delinquencies_params.len(), 1); - assert_eq!(paid_delinquencies_params[0], PaymentThresholds::default()); - let scan_for_new_payables_notify_params = - scan_for_new_payables_notify_params_arc.lock().unwrap(); - let default_scan_intervals = ScanIntervals::default(); - assert_eq!( - *scan_for_new_payables_notify_params, - vec![ScanForNewPayables { - response_skeleton_opt: None - }] + assert!( + pending_payable_params.is_empty(), + "Should be empty but was {:?}", + pending_payable_params + ); + let scan_for_pending_payables_notify_later_params = + scan_for_pending_payables_notify_later_params_arc + .lock() + .unwrap(); + // The part with executing the scan for NewPayables is deliberately omitted here, but + // the scan for pending payables would've been scheduled just after that + assert!( + scan_for_pending_payables_notify_later_params.is_empty(), + "We did not expect to see another schedule for pending payables, but it happened {:?}", + scan_for_pending_payables_notify_later_params ); + // Assertions on the payable scanner proper functioning + // First, there is no functionality from the payable scanner actually running. + // We only witness it to be scheduled. let scan_for_new_payables_notify_later_params = scan_for_new_payables_notify_later_params_arc .lock() .unwrap(); + assert_eq!( + *scan_for_new_payables_notify_later_params, + vec![( + ScanForNewPayables { + response_skeleton_opt: None + }, + new_payable_expected_computed_interval + )] + ); + let mut compute_interval_params = compute_interval_params_arc.lock().unwrap(); + let (p_scheduling_now, last_new_payable_scan_timestamp, _) = + compute_interval_params.remove(0); + assert_eq!(last_new_payable_scan_timestamp, UNIX_EPOCH); + let scan_for_new_payables_notify_params = + scan_for_new_payables_notify_params_arc.lock().unwrap(); assert!( - scan_for_new_payables_notify_later_params.is_empty(), - "Was meant to be empty but contained: {:?}", - scan_for_new_payables_notify_later_params + scan_for_new_payables_notify_params.is_empty(), + "We did not expect any immediate scheduling of new payables, but it happened {:?}", + scan_for_new_payables_notify_params + ); + let scan_for_retry_payables_notify_params = + scan_for_retry_payables_notify_params_arc.lock().unwrap(); + assert!( + scan_for_retry_payables_notify_params.is_empty(), + "We did not expect any scheduling of retry payables, but it happened {:?}", + scan_for_retry_payables_notify_params + ); + // Assertions on the receivable scanner proper functioning + let mut receivable_start_scan_params = receivable_start_scan_params_arc.lock().unwrap(); + let (r_wallet, r_started_at, r_response_skeleton_opt, r_logger, r_trigger_msg_name_str) = + receivable_start_scan_params.remove(0); + assert_eq!(r_wallet, earning_wallet); + assert_eq!(r_response_skeleton_opt, None); + assert!( + r_trigger_msg_name_str.contains("Receivable"), + "Should contain Receivable but {}", + r_trigger_msg_name_str + ); + assert!( + receivable_start_scan_params.is_empty(), + "Should be already empty but was {:?}", + receivable_start_scan_params + ); + let scan_for_receivables_notify_later_params = + scan_for_receivables_notify_later_params_arc.lock().unwrap(); + assert_eq!( + *scan_for_receivables_notify_later_params, + vec![( + ScanForReceivables { + response_skeleton_opt: None + }, + new_payable_expected_computed_interval + )] ); let scan_for_receivables_notify_later_params = scan_for_receivables_notify_later_params_arc.lock().unwrap(); + let default_scan_intervals = ScanIntervals::default(); assert_eq!( *scan_for_receivables_notify_later_params, vec![( @@ -2516,6 +2700,11 @@ mod tests { default_scan_intervals.receivable_scan_interval )] ); + // Finally, an assertion to prove course of actions in time. We expect the pending payable + // scanner to take place before the payable scanner. + // I do believe it's impossible that these two instants would happen at the same time, until + // this is run on a real super-giga-computer. + assert!(pp_scan_started_at < p_scheduling_now); let tlh = TestLogHandler::new(); tlh.exists_log_containing("INFO: Accountant: Scanning for pending payable"); tlh.exists_log_containing(&format!( @@ -2525,6 +2714,63 @@ mod tests { tlh.exists_log_containing("INFO: Accountant: Scanning for delinquencies"); } + #[test] + fn initial_pending_payable_scan_if_potent() { + let pending_payable_dao = PendingPayableDaoMock::default() + .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]); + let mut subject = AccountantBuilder::default() + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + subject.request_transaction_receipts_sub_opt = Some(make_recorder().0.start().recipient()); + let flag_before = subject.scanners.initial_pending_payable_scan(); + + let hint = subject.handle_request_of_scan_for_pending_payable(None); + + let flag_after = subject.scanners.initial_pending_payable_scan(); + assert_eq!(hint, ScanScheduleHint::DoNotSchedule); + assert_eq!(flag_before, true); + assert_eq!(flag_after, false); + } + + #[test] + fn initial_pending_payable_scan_if_impotent() { + let pending_payable_dao = + PendingPayableDaoMock::default().return_all_errorless_fingerprints_result(vec![]); + let mut subject = AccountantBuilder::default() + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let flag_before = subject.scanners.initial_pending_payable_scan(); + + let hint = subject.handle_request_of_scan_for_pending_payable(None); + + let flag_after = subject.scanners.initial_pending_payable_scan(); + assert_eq!(hint, ScanScheduleHint::Schedule); + assert_eq!(flag_before, true); + assert_eq!(flag_after, false); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: Initial scan for pending payable unexpected error: bluh" + )] + fn initial_pending_payable_scan_hits_unexpected_error() { + init_test_logging(); + let mut subject = AccountantBuilder::default().build(); + let pending_payable_scanner = + ScannerMock::default().start_scan_result(Err(StartScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Payables, + started_at: SystemTime::now(), + })); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + let flag_before = subject.scanners.initial_pending_payable_scan(); + + let _ = subject.handle_request_of_scan_for_pending_payable(None); + } + #[test] fn periodical_scanning_for_receivables_and_delinquencies_works() { init_test_logging(); @@ -2537,7 +2783,7 @@ mod tests { .scan_started_at_result(None) .scan_started_at_result(None) .start_scan_params(&start_scan_params_arc) - .start_scan_result(Err(BeginScanError::NothingToProcess)) + .start_scan_result(Err(StartScanError::NothingToProcess)) .start_scan_result(Ok(RetrieveTransactions { recipient: make_wallet("some_recipient"), response_skeleton_opt: None, @@ -2588,12 +2834,14 @@ mod tests { first_attempt_timestamp, first_attempt_response_skeleton_opt, first_attempt_logger, + _, ) = start_scan_params.remove(0); let ( second_attempt_wallet, second_attempt_timestamp, second_attempt_response_skeleton_opt, second_attempt_logger, + _, ) = start_scan_params.remove(0); assert_eq!(first_attempt_wallet, earning_wallet); assert_eq!(second_attempt_wallet, earning_wallet); @@ -2783,7 +3031,7 @@ mod tests { let time_after = SystemTime::now(); let tlh = TestLogHandler::new(); let mut start_scan_payable_params = start_scan_payable_params_arc.lock().unwrap(); - let (wallet, timestamp, response_skeleton_opt, logger) = + let (wallet, timestamp, response_skeleton_opt, logger, _) = start_scan_payable_params.remove(0); assert_eq!(wallet, consuming_wallet); assert!(time_before <= timestamp && timestamp <= time_after); @@ -2792,7 +3040,7 @@ mod tests { debug!(logger, "verifying payable scanner logger"); let mut start_scan_pending_payable_params = start_scan_pending_payable_params_arc.lock().unwrap(); - let (wallet, timestamp, response_skeleton_opt, logger) = + let (wallet, timestamp, response_skeleton_opt, logger, _) = start_scan_pending_payable_params.remove(0); assert_eq!(wallet, consuming_wallet); assert!(time_before <= timestamp && timestamp <= time_after); @@ -3968,7 +4216,7 @@ mod tests { let expected_computed_interval = Duration::from_secs(3); let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() .compute_interval_params(&compute_interval_params_arc) - .compute_interval_results(Some(expected_computed_interval)); + .compute_interval_result(Some(expected_computed_interval)); subject.scan_schedulers.payable.nominal_interval = nominal_interval; subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); subject @@ -4044,7 +4292,7 @@ mod tests { let nominal_interval = Duration::from_secs(6); let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() .compute_interval_params(&compute_interval_params_arc) - .compute_interval_results(None); + .compute_interval_result(None); subject.scan_schedulers.payable.nominal_interval = nominal_interval; subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); subject diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 9b5633e61..f03af703e 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -56,7 +56,7 @@ use crate::db_config::persistent_configuration::{PersistentConfiguration, Persis pub struct Scanners { payable: Box, aware_of_unresolved_pending_payable: bool, - initial_scan: bool, + initial_pending_payable_scan: bool, pending_payable: Box< dyn PrivateScanner< ScanForPendingPayables, @@ -111,7 +111,7 @@ impl Scanners { Scanners { payable, aware_of_unresolved_pending_payable: false, - initial_scan: true, + initial_pending_payable_scan: true, pending_payable, receivable, } @@ -124,16 +124,16 @@ impl Scanners { response_skeleton_opt: Option, logger: &Logger, automatic_scans_enabled: bool, - ) -> Result { + ) -> Result { let triggered_manually = response_skeleton_opt.is_some(); if triggered_manually && automatic_scans_enabled { - return Err(BeginScanError::ManualTriggerError( + return Err(StartScanError::ManualTriggerError( MTError::AutomaticScanConflict, )); } if let Some(started_at) = self.payable.scan_started_at() { - return Err(BeginScanError::ScanAlreadyRunning { + return Err(StartScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Payables, started_at, }); @@ -156,14 +156,14 @@ impl Scanners { timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result { + ) -> Result { if let Some(started_at) = self.payable.scan_started_at() { unreachable!( "Guards should ensure that no payable scanner can run if the pending payable \ repetitive sequence is still ongoing. However, some other payable scan intruded \ at {} and is still running at {}", - BeginScanError::timestamp_as_string(started_at), - BeginScanError::timestamp_as_string(SystemTime::now()) + StartScanError::timestamp_as_string(started_at), + StartScanError::timestamp_as_string(SystemTime::now()) ) } @@ -183,7 +183,7 @@ impl Scanners { response_skeleton_opt: Option, logger: &Logger, automatic_scans_enabled: bool, - ) -> Result { + ) -> Result { let triggered_manually = response_skeleton_opt.is_some(); self.check_general_conditions_for_pending_payable_scan( triggered_manually, @@ -200,18 +200,18 @@ impl Scanners { unreachable!( "Both payable scanners should never be allowed to run in parallel. Scan for \ pending payables started at: {}, scan for payables started at: {}", - BeginScanError::timestamp_as_string(pp_timestamp), - BeginScanError::timestamp_as_string(p_timestamp) + StartScanError::timestamp_as_string(pp_timestamp), + StartScanError::timestamp_as_string(p_timestamp) ) } (Some(started_at), None) => { - return Err(BeginScanError::ScanAlreadyRunning { + return Err(StartScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::PendingPayables, started_at, }) } (None, Some(started_at)) => { - return Err(BeginScanError::ScanAlreadyRunning { + return Err(StartScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Payables, started_at, }) @@ -229,16 +229,16 @@ impl Scanners { response_skeleton_opt: Option, logger: &Logger, automatic_scans_enabled: bool, - ) -> Result { + ) -> Result { let triggered_manually = response_skeleton_opt.is_some(); if triggered_manually && automatic_scans_enabled { - return Err(BeginScanError::ManualTriggerError( + return Err(StartScanError::ManualTriggerError( MTError::AutomaticScanConflict, )); } if let Some(started_at) = self.receivable.scan_started_at() { - return Err(BeginScanError::ScanAlreadyRunning { + return Err(StartScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Receivables, started_at, }); @@ -296,13 +296,21 @@ impl Scanners { todo!() } + pub fn initial_pending_payable_scan(&self) -> bool { + self.initial_pending_payable_scan + } + + pub fn unset_initial_pending_payable_scan(&mut self) { + self.initial_pending_payable_scan = false + } + fn start_correct_payable_scanner<'a, TriggerMessage>( scanner: &'a mut (dyn MultistageDualPayableScanner + 'a), wallet: &Wallet, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result + ) -> Result where TriggerMessage: Message, (dyn MultistageDualPayableScanner + 'a): @@ -318,15 +326,16 @@ impl Scanners { &mut self, triggered_manually: bool, automatic_scans_enabled: bool, - ) -> Result<(), BeginScanError> { + ) -> Result<(), StartScanError> { if triggered_manually && automatic_scans_enabled { - Err(BeginScanError::ManualTriggerError( + Err(StartScanError::ManualTriggerError( MTError::AutomaticScanConflict, )) - } else if self.initial_scan { - todo!("other conditions forgiven") + } else if self.initial_pending_payable_scan { + self.initial_pending_payable_scan = false; + Ok(()) } else if triggered_manually && !self.aware_of_unresolved_pending_payable { - Err(BeginScanError::ManualTriggerError( + Err(StartScanError::ManualTriggerError( MTError::UnnecessaryRequest { hint_opt: Some("Run the Payable scanner first.".to_string()), }, @@ -362,7 +371,7 @@ where timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result; + ) -> Result; } trait Scanner @@ -452,7 +461,7 @@ impl StartableScanner for PayableS timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result { + ) -> Result { self.mark_as_started(timestamp); info!(logger, "Scanning for new payables"); let all_non_pending_payables = self.payable_dao.non_pending_payables(); @@ -469,7 +478,7 @@ impl StartableScanner for PayableS match qualified_payables.is_empty() { true => { self.mark_as_ended(logger); - Err(BeginScanError::NothingToProcess) + Err(StartScanError::NothingToProcess) } false => { info!( @@ -496,7 +505,7 @@ impl StartableScanner for Payabl _timestamp: SystemTime, _response_skeleton_opt: Option, _logger: &Logger, - ) -> Result { + ) -> Result { todo!() } } @@ -851,14 +860,14 @@ impl StartableScanner timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result { + ) -> Result { self.mark_as_started(timestamp); info!(logger, "Scanning for pending payable"); let filtered_pending_payable = self.pending_payable_dao.return_all_errorless_fingerprints(); match filtered_pending_payable.is_empty() { true => { self.mark_as_ended(logger); - Err(BeginScanError::NothingToProcess) + Err(StartScanError::NothingToProcess) } false => { debug!( @@ -1107,7 +1116,7 @@ impl StartableScanner for ReceivableSc timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result { + ) -> Result { self.mark_as_started(timestamp); info!(logger, "Scanning for receivables to {}", earning_wallet); self.scan_for_delinquencies(timestamp, logger); @@ -1126,9 +1135,9 @@ impl Scanner> for ReceivableScanner { // timestamp: SystemTime, // response_skeleton_opt: Option, // logger: &Logger, - // ) -> Result { + // ) -> Result { // if let Some(timestamp) = self.scan_started_at() { - // return Err(BeginScanError::ScanAlreadyRunning(timestamp)); + // return Err(StartScanError::ScanAlreadyRunning(timestamp)); // } // self.mark_as_started(timestamp); // info!(logger, "Scanning for receivables to {}", earning_wallet); @@ -1276,7 +1285,7 @@ impl ReceivableScanner { } #[derive(Debug, PartialEq, Eq)] -pub enum BeginScanError { +pub enum StartScanError { NothingToProcess, NoConsumingWalletFound, ScanAlreadyRunning { @@ -1287,7 +1296,7 @@ pub enum BeginScanError { ManualTriggerError(MTError), } -impl BeginScanError { +impl StartScanError { pub fn log_error(&self, logger: &Logger, scan_type: ScanType, is_externally_triggered: bool) { enum ErrorType { Temporary(String), @@ -1295,26 +1304,26 @@ impl BeginScanError { } let log_message = match self { - BeginScanError::NothingToProcess => ErrorType::Temporary(format!( + StartScanError::NothingToProcess => ErrorType::Temporary(format!( "There was nothing to process during {:?} scan.", scan_type )), - BeginScanError::ScanAlreadyRunning { + StartScanError::ScanAlreadyRunning { pertinent_scanner, started_at, } => ErrorType::Temporary(Self::scan_already_running_msg( *pertinent_scanner, *started_at, )), - BeginScanError::NoConsumingWalletFound => ErrorType::Permanent(format!( + StartScanError::NoConsumingWalletFound => ErrorType::Permanent(format!( "Cannot initiate {:?} scan because no consuming wallet was found.", scan_type )), - BeginScanError::CalledFromNullScanner => match cfg!(test) { + StartScanError::CalledFromNullScanner => match cfg!(test) { true => todo!(), //None, false => panic!("Null Scanner shouldn't be running inside production code."), }, - BeginScanError::ManualTriggerError(e) => match e { + StartScanError::ManualTriggerError(e) => match e { MTError::AutomaticScanConflict => ErrorType::Permanent(format!( "Manual {:?} scan was denied. Automatic scanning setup prevents manual \ triggers.", @@ -1337,12 +1346,9 @@ impl BeginScanError { ErrorType::Permanent(msg) => warning!(logger, "{}", msg), }, - false => { - match log_message { - ErrorType::Temporary(msg) => todo!(), //debug!(logger, "{}", log_message), - ErrorType::Permanent(msg) => todo!(), - } - } + false => match log_message { + ErrorType::Temporary(msg) | ErrorType::Permanent(msg) => debug!(logger, "{}", msg), + }, } } @@ -1363,7 +1369,7 @@ impl BeginScanError { format!( "{:?} scan was already initiated at {}. Hence, this scan request will be ignored.", error_pertinent_scanner, - BeginScanError::timestamp_as_string(error_pertinent_scanner_started) + StartScanError::timestamp_as_string(error_pertinent_scanner_started) ) } } @@ -1381,8 +1387,8 @@ pub mod local_test_utils { use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; use crate::accountant::scanners::{ - BeginScanError, MultistageDualPayableScanner, PreparedAdjustment, PrivateScanner, Scanner, - SolvencySensitivePaymentInstructor, StartableScanner, + MultistageDualPayableScanner, PreparedAdjustment, PrivateScanner, Scanner, + SolvencySensitivePaymentInstructor, StartScanError, StartableScanner, }; use crate::accountant::OutboundPaymentsInstructions; use crate::accountant::{ @@ -1395,6 +1401,7 @@ pub mod local_test_utils { use masq_lib::logger::Logger; use masq_lib::ui_gateway::NodeToUiMessage; use rand::distributions::Standard; + use std::any::type_name; use std::cell::RefCell; use std::sync::{Arc, Mutex}; use std::time::SystemTime; @@ -1421,8 +1428,8 @@ pub mod local_test_utils { _timestamp: SystemTime, _response_skeleton_opt: Option, _logger: &Logger, - ) -> Result { - Err(BeginScanError::CalledFromNullScanner) + ) -> Result { + Err(StartScanError::CalledFromNullScanner) } } @@ -1482,8 +1489,9 @@ pub mod local_test_utils { } pub struct ScannerMock { - start_scan_params: Arc, Logger)>>>, - start_scan_results: RefCell>>, + start_scan_params: + Arc, Logger, String)>>>, + start_scan_results: RefCell>>, finish_scan_params: Arc>>, finish_scan_results: RefCell>, scan_started_at_results: RefCell>>, @@ -1514,12 +1522,15 @@ pub mod local_test_utils { timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result { + ) -> Result { self.start_scan_params.lock().unwrap().push(( wallet.clone(), timestamp, response_skeleton_opt, logger.clone(), + // This serves for identification in scanners allowing different modes to start + // them up through. + type_name::().to_string(), )); if self.is_allowed_to_stop_the_system() && self.is_last_message() { System::current().stop(); @@ -1580,13 +1591,15 @@ pub mod local_test_utils { pub fn start_scan_params( mut self, - params: &Arc, Logger)>>>, + params: &Arc< + Mutex, Logger, String)>>, + >, ) -> Self { self.start_scan_params = params.clone(); self } - pub fn start_scan_result(self, result: Result) -> Self { + pub fn start_scan_result(self, result: Result) -> Self { self.start_scan_results.borrow_mut().push(result); self } @@ -1677,7 +1690,7 @@ mod tests { use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult, PendingPayableMetadata}; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport, PendingPayableScanResult}; - use crate::accountant::scanners::{Scanner, BeginScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, PrivateScanner}; + use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, PrivateScanner}; use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanForNewPayables, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; @@ -1771,10 +1784,6 @@ mod tests { self.aware_of_unresolved_pending_payable = value } - pub fn set_initial_scan(&mut self, value: bool) { - self.initial_scan = value - } - fn simple_scanner_timestamp_treatment( scanner: &mut Scanner, value: MarkScanner, @@ -1856,7 +1865,7 @@ mod tests { ); assert_eq!(payable_scanner.common.initiated_at_opt.is_some(), false); assert_eq!(scanners.aware_of_unresolved_pending_payable, false); - assert_eq!(scanners.initial_scan, true); + assert_eq!(scanners.initial_pending_payable_scan, true); assert_eq!( pending_payable_scanner.when_pending_too_long_sec, when_pending_too_long_sec @@ -1975,7 +1984,7 @@ mod tests { assert_eq!(is_scan_running, true); assert_eq!( result, - Err(BeginScanError::ScanAlreadyRunning { + Err(StartScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Payables, started_at: previous_scan_started_at }) @@ -2004,7 +2013,7 @@ mod tests { let is_scan_running = subject.scan_started_at().is_some(); assert_eq!(is_scan_running, false); - assert_eq!(result, Err(BeginScanError::NothingToProcess)); + assert_eq!(result, Err(StartScanError::NothingToProcess)); } #[test] @@ -2072,7 +2081,7 @@ mod tests { ); let caught_panic = catch_unwind(AssertUnwindSafe(|| { - let _: Result = subject + let _: Result = subject .start_retry_payable_scan_guarded( &consuming_wallet, SystemTime::now(), @@ -3132,7 +3141,7 @@ mod tests { assert_eq!(is_scan_running, true); assert_eq!( result, - Err(BeginScanError::ScanAlreadyRunning { + Err(StartScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::PendingPayables, started_at: now }) @@ -3163,7 +3172,7 @@ mod tests { assert_eq!(is_scan_running, false); assert_eq!( result, - Err(BeginScanError::ScanAlreadyRunning { + Err(StartScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Payables, started_at: previous_scan_started_at }) @@ -3268,10 +3277,21 @@ mod tests { pending_payable_scanner.start_scan(&consuming_wallet, now, None, &Logger::new("test")); let is_scan_running = pending_payable_scanner.scan_started_at().is_some(); - assert_eq!(result, Err(BeginScanError::NothingToProcess)); + assert_eq!(result, Err(StartScanError::NothingToProcess)); assert_eq!(is_scan_running, false); } + #[test] + fn check_general_conditions_for_pending_payable_scan_if_it_is_initial_pending_payable_scan() { + let mut subject = make_dull_subject(); + subject.initial_pending_payable_scan = true; + + let result = subject.check_general_conditions_for_pending_payable_scan(false, true); + + assert_eq!(result, Ok(())); + assert_eq!(subject.initial_pending_payable_scan, true); + } + fn assert_interpreting_none_status_for_pending_payable( test_name: &str, when_pending_too_long_sec: u64, @@ -3964,7 +3984,7 @@ mod tests { assert_eq!(is_scan_running, true); assert_eq!( result, - Err(BeginScanError::ScanAlreadyRunning { + Err(StartScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Receivables, started_at: now }) @@ -4396,7 +4416,7 @@ mod tests { let still_running_scanner = ScanType::PendingPayables; let time = SystemTime::now(); - let result = BeginScanError::scan_already_running_msg(still_running_scanner, time); + let result = StartScanError::scan_already_running_msg(still_running_scanner, time); let expected_first_fragment = "PendingPayables scan was already initiated at"; assert!( @@ -4426,7 +4446,7 @@ mod tests { Scanners { payable: Box::new(NullScanner::new()), aware_of_unresolved_pending_payable: false, - initial_scan: false, + initial_pending_payable_scan: false, pending_payable: Box::new(NullScanner::new()), receivable: Box::new(NullScanner::new()), } @@ -4463,7 +4483,7 @@ mod tests { impl From for PseudoTimestamp { fn from(timestamp: SystemTime) -> Self { - let specially_formatted_timestamp = BeginScanError::timestamp_as_string(timestamp); + let specially_formatted_timestamp = StartScanError::timestamp_as_string(timestamp); let regex = Self::regex(); let mut captures = regex.captures_iter(&specially_formatted_timestamp); PseudoTimestamp::new_from_captures(&mut captures) diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 4830a94c7..b05a05bf4 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -16,9 +16,9 @@ use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; pub struct ScanSchedulers { - pub payable: PayableScanScheduler, - pub pending_payable: SimplePeriodicalScanScheduler, - pub receivable: SimplePeriodicalScanScheduler, + pub payable: PayableScanScheduler, + pub pending_payable: SimplePeriodicalScanScheduler, + pub receivable: SimplePeriodicalScanScheduler, pub automatic_scans_enabled: bool, } @@ -46,21 +46,22 @@ pub enum ScanScheduleHint { DoNotSchedule, } -pub struct PayableScanScheduler { - pub new_payable_notify_later: Box>, +pub enum HintableScanners { + NewPayables, + RetryPayables, + PendingPayables, +} + +pub struct PayableScanScheduler { + pub new_payable_notify_later: Box>, pub dyn_interval_computer: Box, pub inner: Arc>, pub nominal_interval: Duration, - pub new_payable_notify: Box>, - pub retry_payable_notify: Box>, + pub new_payable_notify: Box>, + pub retry_payable_notify: Box>, } -impl PayableScanScheduler -where - ActorType: Actor> - + Handler - + Handler, -{ +impl PayableScanScheduler { fn new(nominal_interval: Duration) -> Self { Self { new_payable_notify_later: Box::new(NotifyLaterHandleReal::default()), @@ -72,11 +73,7 @@ where } } - pub fn schedule_for_new_payable( - &self, - ctx: &mut Context, - response_skeleton_opt: Option, - ) { + pub fn schedule_for_new_payable(&self, ctx: &mut Context) { let inner = self.inner.lock().expect("couldn't acquire inner"); let last_new_payable_timestamp = inner.last_new_payable_scan_timestamp; let nominal_interval = self.nominal_interval; @@ -108,7 +105,7 @@ where // the Accountant's mailbox with no delay pub fn schedule_for_retry_payable( &self, - ctx: &mut Context, + ctx: &mut Context, response_skeleton_opt: Option, ) { self.retry_payable_notify.notify( @@ -134,32 +131,6 @@ impl Default for PayableScanSchedulerInner { } } -pub trait AutomaticSchedulingAwareScanner { - fn is_currently_automatically_scheduled(&self, spec: ScannerSpecification) -> bool; - fn mark_as_automatically_scheduled(&self, spec: ScannerSpecification); - fn mark_as_already_automatically_utilized(&self, spec: ScannerSpecification); -} - -impl AutomaticSchedulingAwareScanner for PayableScanScheduler { - fn is_currently_automatically_scheduled(&self, spec: PayableScannerMode) -> bool { - todo!() - } - - fn mark_as_automatically_scheduled(&self, spec: PayableScannerMode) { - todo!() - } - - fn mark_as_already_automatically_utilized(&self, spec: PayableScannerMode) { - todo!() - } -} - -#[derive(Clone, Copy)] -pub enum PayableScannerMode { - Retry, - NewPayable, -} - pub trait NewPayableScanDynIntervalComputer { fn compute_interval( &self, @@ -195,73 +166,40 @@ impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerReal } } -pub struct SimplePeriodicalScanScheduler { - phantom: PhantomData, +pub struct SimplePeriodicalScanScheduler { pub is_currently_automatically_scheduled: RefCell, - pub handle: Box>, + pub handle: Box>, pub interval: Duration, } -impl SimplePeriodicalScanScheduler +impl SimplePeriodicalScanScheduler where Message: actix::Message + Default + 'static + Send, - ActorType: Actor> + Handler, + Accountant: Actor + Handler, { fn new(interval: Duration) -> Self { Self { - phantom: PhantomData::default(), is_currently_automatically_scheduled: RefCell::new(false), handle: Box::new(NotifyLaterHandleReal::default()), interval, } } - pub fn schedule( - &self, - ctx: &mut Context, - response_skeleton_opt: Option, - ) { + pub fn schedule(&self, ctx: &mut Context) { // The default of the message implies response_skeleton_opt to be None because scheduled // scans don't respond let _ = self .handle .notify_later(Message::default(), self.interval, ctx); - - if response_skeleton_opt == None { - self.mark_as_automatically_scheduled(()) - } - } -} - -impl AutomaticSchedulingAwareScanner<()> for SimplePeriodicalScanScheduler { - fn is_currently_automatically_scheduled(&self, _spec: ()) -> bool { - *self.is_currently_automatically_scheduled.borrow() - } - - fn mark_as_automatically_scheduled(&self, _spec: ()) { - self.is_currently_automatically_scheduled.replace(true); - } - - fn mark_as_already_automatically_utilized(&self, _spec: ()) { - self.is_currently_automatically_scheduled.replace(false); } } #[cfg(test)] mod tests { use crate::accountant::scanners::scan_schedulers::{ - AutomaticSchedulingAwareScanner, NewPayableScanDynIntervalComputer, - NewPayableScanDynIntervalComputerReal, PayableScanScheduler, PayableScanSchedulerError, - PayableScanSchedulerInner, PayableScannerMode, ScanSchedulers, - SimplePeriodicalScanScheduler, - }; - use crate::accountant::{ - ResponseSkeleton, ScanForNewPayables, ScanForPendingPayables, ScanForReceivables, - ScanForRetryPayables, SkeletonOptHolder, + NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, + ScanSchedulers, }; use crate::sub_lib::accountant::ScanIntervals; - use crate::test_utils::unshared_test_utils::system_killer_actor::SystemKillerActor; - use actix::{Actor, Context, Handler, Message, System}; - use std::marker::PhantomData; use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[test] @@ -419,153 +357,4 @@ mod tests { Duration::from_secs(32), ); } - - #[derive(Message)] - struct TestSetupCarrierMsg { - scheduler: Scheduler, - automated_message: MessageToSchedule, - user_triggered_message: MessageToSchedule, - schedule_scanner: ScheduleScanner, - scan_specs: ScannerSpecs, - } - - struct ActixContextProvidingActor {} - - impl Actor for ActixContextProvidingActor { - type Context = Context; - } - - impl Handler for ActixContextProvidingActor { - type Result = (); - - fn handle(&mut self, _msg: ScanForNewPayables, _ctx: &mut Self::Context) -> Self::Result { - intentionally_blank!() - } - } - - impl Handler for ActixContextProvidingActor { - type Result = (); - - fn handle(&mut self, _msg: ScanForRetryPayables, _ctx: &mut Self::Context) -> Self::Result { - intentionally_blank!() - } - } - - impl Handler for ActixContextProvidingActor { - type Result = (); - - fn handle(&mut self, _msg: ScanForReceivables, _ctx: &mut Self::Context) -> Self::Result { - intentionally_blank!() - } - } - - impl - Handler> - for ActixContextProvidingActor - where - ScheduleScanner: Fn(&Scheduler, MessageToSchedule, &mut Self::Context), - Scheduler: AutomaticSchedulingAwareScanner, - ScannerSpecs: Copy, - { - type Result = (); - - fn handle( - &mut self, - msg: TestSetupCarrierMsg, - ctx: &mut Self::Context, - ) -> Self::Result { - let scheduler = msg.scheduler; - let scan_specs = msg.scan_specs; - - let first_state = scheduler.is_currently_automatically_scheduled(scan_specs); - scheduler.mark_as_automatically_scheduled(scan_specs); - let second_state = scheduler.is_currently_automatically_scheduled(scan_specs); - scheduler.mark_as_already_automatically_utilized(scan_specs); - let third_state = scheduler.is_currently_automatically_scheduled(scan_specs); - (&msg.schedule_scanner)(&scheduler, msg.automated_message, ctx); - let fourth_state = scheduler.is_currently_automatically_scheduled(scan_specs); - scheduler.mark_as_already_automatically_utilized(scan_specs); - let fifth_state = scheduler.is_currently_automatically_scheduled(scan_specs); - (&msg.schedule_scanner)(&scheduler, msg.user_triggered_message, ctx); - let sixth_state = scheduler.is_currently_automatically_scheduled(scan_specs); - - assert_eq!(first_state, false); - assert_eq!(second_state, true); - assert_eq!(third_state, false); - assert_eq!(fourth_state, true); - assert_eq!(fifth_state, false); - assert_eq!(sixth_state, false); - System::current().stop(); - } - } - - #[test] - fn scheduling_registration_on_simple_periodical_scanner_works() { - let system = System::new("test"); - let system_killer = SystemKillerActor::new(Duration::from_secs(10)); - system_killer.start(); - let test_performer_addr = ActixContextProvidingActor {}.start(); - let duration = Duration::from_secs(1000); - let scheduler = SimplePeriodicalScanScheduler::new(duration); - let automated_message = ScanForReceivables { - response_skeleton_opt: None, - }; - let user_triggered_message = ScanForReceivables { - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 12, - context_id: 7, - }), - }; - let schedule_scanner = - |scheduler: &SimplePeriodicalScanScheduler< - ScanForReceivables, - ActixContextProvidingActor, - >, - msg: ScanForReceivables, - ctx: &mut Context| { - scheduler.schedule(ctx, msg.response_skeleton_opt); - }; - let scan_specs = (); - let msg = TestSetupCarrierMsg { - scheduler, - automated_message, - user_triggered_message, - schedule_scanner, - scan_specs, - }; - - test_performer_addr.try_send(msg).unwrap(); - - assert_eq!(system.run(), 0) - } - - #[test] - fn scheduling_registration_for_new_payable_on_notify_later_handle_works_in_complex_scheduler() { - todo!( - "maybe now first consider where it's gonna help you to know the current schedule state" - ) - // let system = System::new("test"); - // let system_killer = SystemKillerActor::new(Duration::from_secs(10)); - // system_killer.start(); - // let test_performer_addr = ActixContextProvidingActor {}.start(); - // let duration = Duration::from_secs(1000); - // let scheduler = PayableScanScheduler::new(duration); - // let automated_message = ScanForNewPayables{ response_skeleton_opt: None }; - // let user_triggered_message = ScanForNewPayables{response_skeleton_opt: Some(ResponseSkeleton{ client_id: 12, context_id: 7 })}; - // let schedule_scanner = |scheduler: &PayableScanScheduler, msg: ScanForReceivables, ctx: &mut Context|{ - // scheduler.schedule_for_new_payable(ctx, msg.response_skeleton_opt); - // }; - // let scan_specs = PayableScannerMode::NewPayable; - // let msg = TestSetupCarrierMsg { - // scheduler, - // automated_message, - // user_triggered_message, - // schedule_scanner, - // scan_specs, - // }; - // - // test_performer_addr.try_send(msg).unwrap(); - // - // assert_eq!(system.run(), 0) - } } diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 7688ccb6b..a916e2dd2 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -50,7 +50,7 @@ impl NewPayableScanDynIntervalComputerMock { self } - pub fn compute_interval_results(self, result: Option) -> Self { + pub fn compute_interval_result(self, result: Option) -> Self { self.compute_interval_results.borrow_mut().push(result); self } diff --git a/node/src/sub_lib/peer_actors.rs b/node/src/sub_lib/peer_actors.rs index 3a51be868..571eca3fe 100644 --- a/node/src/sub_lib/peer_actors.rs +++ b/node/src/sub_lib/peer_actors.rs @@ -14,6 +14,8 @@ use std::fmt::Debug; use std::fmt::Formatter; use std::net::IpAddr; +// TODO This file should be test only + #[derive(Clone, PartialEq, Eq)] pub struct PeerActors { pub proxy_server: ProxyServerSubs, diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index 509529780..806328883 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -28,6 +28,7 @@ use crate::sub_lib::hopper::{HopperSubs, MessageType}; use crate::sub_lib::neighborhood::NeighborhoodSubs; use crate::sub_lib::neighborhood::{ConfigChangeMsg, ConnectionProgressMessage}; +use crate::proxy_server::ProxyServer; use crate::sub_lib::configurator::ConfiguratorSubs; use crate::sub_lib::neighborhood::NodeQueryResponseMetadata; use crate::sub_lib::neighborhood::RemoveNeighborMessage; @@ -638,8 +639,9 @@ impl PeerActorsBuilder { self } - // This must be called after System.new and before System.run - pub fn build(self) -> PeerActors { + // This must be called after System.new and before System.run. + // These addresses may be helpful for setting up the Counter Messages. + pub fn build_and_provide_addresses(self) -> (PeerActors, PeerActorAddrs) { let proxy_server_addr = self.proxy_server.start(); let dispatcher_addr = self.dispatcher.start(); let hopper_addr = self.hopper.start(); @@ -650,20 +652,53 @@ impl PeerActorsBuilder { let blockchain_bridge_addr = self.blockchain_bridge.start(); let configurator_addr = self.configurator.start(); - PeerActors { - proxy_server: make_proxy_server_subs_from_recorder(&proxy_server_addr), - dispatcher: make_dispatcher_subs_from_recorder(&dispatcher_addr), - hopper: make_hopper_subs_from_recorder(&hopper_addr), - proxy_client_opt: Some(make_proxy_client_subs_from_recorder(&proxy_client_addr)), - neighborhood: make_neighborhood_subs_from_recorder(&neighborhood_addr), - accountant: make_accountant_subs_from_recorder(&accountant_addr), - ui_gateway: make_ui_gateway_subs_from_recorder(&ui_gateway_addr), - blockchain_bridge: make_blockchain_bridge_subs_from_recorder(&blockchain_bridge_addr), - configurator: make_configurator_subs_from_recorder(&configurator_addr), - } + ( + PeerActors { + proxy_server: make_proxy_server_subs_from_recorder(&proxy_server_addr), + dispatcher: make_dispatcher_subs_from_recorder(&dispatcher_addr), + hopper: make_hopper_subs_from_recorder(&hopper_addr), + proxy_client_opt: Some(make_proxy_client_subs_from_recorder(&proxy_client_addr)), + neighborhood: make_neighborhood_subs_from_recorder(&neighborhood_addr), + accountant: make_accountant_subs_from_recorder(&accountant_addr), + ui_gateway: make_ui_gateway_subs_from_recorder(&ui_gateway_addr), + blockchain_bridge: make_blockchain_bridge_subs_from_recorder( + &blockchain_bridge_addr, + ), + configurator: make_configurator_subs_from_recorder(&configurator_addr), + }, + PeerActorAddrs { + proxy_server_addr, + dispatcher_addr, + hopper_addr, + proxy_client_addr, + neighborhood_addr, + accountant_addr, + ui_gateway_addr, + blockchain_bridge_addr, + configurator_addr, + }, + ) + } + + // This must be called after System.new and before System.run + pub fn build(self) -> PeerActors { + let (peer_actors, _) = self.build_and_provide_addresses(); + peer_actors } } +pub struct PeerActorAddrs { + pub proxy_server_addr: Addr, + pub dispatcher_addr: Addr, + pub hopper_addr: Addr, + pub proxy_client_addr: Addr, + pub neighborhood_addr: Addr, + pub accountant_addr: Addr, + pub ui_gateway_addr: Addr, + pub blockchain_bridge_addr: Addr, + pub configurator_addr: Addr, +} + #[cfg(test)] mod tests { use super::*; From fd216cf7653aa9644e94d5b4422ddd665ceafc49 Mon Sep 17 00:00:00 2001 From: Bert Date: Tue, 20 May 2025 16:34:37 +0200 Subject: [PATCH 22/49] GH-602: tests for pending payable scanner and its start scan error resolver are compiling --- node/src/accountant/mod.rs | 116 ++--- node/src/accountant/scanners/mod.rs | 5 +- .../accountant/scanners/scan_schedulers.rs | 397 +++++++++++++++++- 3 files changed, 416 insertions(+), 102 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 43395d7f7..f578e44e7 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -225,8 +225,8 @@ impl Handler for Accountant { // the RetryPayableScanner, which finishes, and the PendingPayablesScanner is scheduled // to run again. Therefore, not from here. let response_skeleton_opt = msg.response_skeleton_opt; - if self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) - == ScanScheduleHint::Schedule + if let ScanScheduleHint::Schedule(ScanType::Payables) = + self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) { todo!() // self.scan_schedulers @@ -240,15 +240,17 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForNewPayables, ctx: &mut Self::Context) -> Self::Result { + // TODO recheck these descriptions // We know this must be a scheduled scanner, but we don't if we are going to reschedule it // from here or elsewhere. If the scan finds no payables that qualify for a payment, we do // it right away. If some new pending payables are produced, the next scheduling is going to // be determined by the PendingPayableScanner, evaluating if it has seen all pending // payables complete. That opens up an opportunity for another run of the NewPayableScanner. let response_skeleton = msg.response_skeleton_opt; - if self.handle_request_of_scan_for_new_payable(response_skeleton) - == ScanScheduleHint::Schedule + if let ScanScheduleHint::Schedule(ScanType::Payables) = + self.handle_request_of_scan_for_new_payable(response_skeleton) { + todo!("make sure we've been here before, and if not, schedule"); self.scan_schedulers.payable.schedule_for_new_payable(ctx) } } @@ -261,7 +263,7 @@ impl Handler for Accountant { // RetryPayableScanner is scheduled only when the PendingPayableScanner finishes finding out // that there have been some failed pending payables. That means not from here. let response_skeleton = msg.response_skeleton_opt; - self.handle_request_of_scan_for_retry_payable(response_skeleton); + let _ = self.handle_request_of_scan_for_retry_payable(response_skeleton); } } @@ -940,18 +942,9 @@ impl Accountant { .expect("UiGateway is dead"); }); - // TODO is this comment correct? - // At scanners with scheduling usually happening at their ultimate end, we must be - // thorough with evaluating errors because an inadequate reaction could disrupt - // the whole scan chain. Scheduling must often be done despite the error. - if e == StartScanError::NothingToProcess { - todo!("Shouldn't we schedule with every error????"); - ScanScheduleHint::Schedule - } else if e == StartScanError::NoConsumingWalletFound { - todo!("schedule") - } else { - ScanScheduleHint::DoNotSchedule - } + self.scan_schedulers + .scan_schedule_hint_from_error_resolver + .resolve_hint_from_error(todo!(), &e, response_skeleton_opt.is_some()) } } } @@ -999,17 +992,9 @@ impl Accountant { // .expect("UiGateway is dead"); // }); - // TODO is this comment correct? - // At scanners with scheduling usually happening at their ultimate end, we must be - // thorough with evaluating errors because an inadequate reaction could disrupt - // the whole scan chain. Scheduling must often be done despite the error. - if e == StartScanError::NothingToProcess { - todo!("unreachable"); - } else if response_skeleton_opt.is_some() { - todo!("do not schedule") - } else { - ScanScheduleHint::Schedule - } + self.scan_schedulers + .scan_schedule_hint_from_error_resolver + .resolve_hint_from_error(todo!(), &e, response_skeleton_opt.is_some()) } } } @@ -1056,46 +1041,10 @@ impl Accountant { }) .expect("UiGateway is dead"); }); - // // TODO is this comment correct? - // // At scanners with scheduling usually happening at their ultimate end, we must be - // // thorough with evaluating errors because an inadequate reaction could disrupt - // // the whole scan chain. Scheduling must often be done despite the error. - // if response_skeleton_opt.is_some() { - // todo!("do not schedule") - // } - // - // if e == StartScanError::NothingToProcess { - // if !self.scanners.initial_pending_payable_scan() { - // todo!("unreachable") - // } - // } - // - // if matches!(e, StartScanError::ScanAlreadyRunning{..}) { - // todo!("nope") - // } - // - // // StartScanError::ManualTriggerError should be included in the condition for - // // externally triggered scans as this error implies only a single way it can occur. - // - // todo!("schedule for the rest")//ScanScheduleHint::Schedule - - // TODO is this comment correct? - // At scanners with scheduling usually happening at their ultimate end, we must be - // thorough with evaluating errors because an inadequate reaction could disrupt - // the whole scan chain. Scheduling must often be done despite the error. - if e == StartScanError::NothingToProcess && response_skeleton_opt.is_none() { - if self.scanners.initial_pending_payable_scan() { - todo!("schedule") - } else { - todo!("unreachable") - } - } else if e == StartScanError::NothingToProcess { - todo!("unreachable") - } else if e == StartScanError::NoConsumingWalletFound { - todo!("schedule") - } else { - todo!("do not schedule") - } + + self.scan_schedulers + .scan_schedule_hint_from_error_resolver + .resolve_hint_from_error(todo!(), &e, response_skeleton_opt.is_some()) } }; @@ -1148,15 +1097,6 @@ impl Accountant { } } - fn derive_scan_schedule_hint_from_error( - &self, - scan_type: ScanType, - e: StartScanError, - response_skeleton_opt: Option, - ) -> ScanScheduleHint { - todo!() - } - fn handle_externally_triggered_scan( &mut self, _ctx: &mut Context, @@ -1923,21 +1863,13 @@ mod tests { "externally_triggered_scan_is_not_handled_in_case_the_scan_is_already_running"; let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); config.automatic_scans_enabled = false; - let fingerprint = PendingPayableFingerprint { - rowid: 1234, - timestamp: SystemTime::now(), - hash: Default::default(), - attempt: 1, - amount: 1_000_000, - process_error: None, - }; - let pending_payable_dao = PendingPayableDaoMock::default() - .return_all_errorless_fingerprints_result(vec![fingerprint]); + let payable_dao = + PayableDaoMock::default().non_pending_payables_result(vec![make_payable_account(123)]); let subject = AccountantBuilder::default() .bootstrapper_config(config) .consuming_wallet(make_paying_wallet(b"consuming")) .logger(Logger::new(test_name)) - .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .payable_daos(vec![ForPayableScanner(payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); @@ -1964,7 +1896,7 @@ mod tests { system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); TestLogHandler::new().exists_log_containing(&format!( - "INFO: {}: PendingPayables scan was already initiated", + "INFO: {}: Payables scan was already initiated", test_name )); assert_eq!(blockchain_bridge_recording.len(), 1); @@ -2715,7 +2647,7 @@ mod tests { } #[test] - fn initial_pending_payable_scan_if_potent() { + fn initial_pending_payable_scan_if_some_payables_found() { let pending_payable_dao = PendingPayableDaoMock::default() .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]); let mut subject = AccountantBuilder::default() @@ -2733,7 +2665,7 @@ mod tests { } #[test] - fn initial_pending_payable_scan_if_impotent() { + fn initial_pending_payable_scan_if_no_payables_found() { let pending_payable_dao = PendingPayableDaoMock::default().return_all_errorless_fingerprints_result(vec![]); let mut subject = AccountantBuilder::default() @@ -2744,7 +2676,7 @@ mod tests { let hint = subject.handle_request_of_scan_for_pending_payable(None); let flag_after = subject.scanners.initial_pending_payable_scan(); - assert_eq!(hint, ScanScheduleHint::Schedule); + assert_eq!(hint, ScanScheduleHint::Schedule(ScanType::Payables)); assert_eq!(flag_before, true); assert_eq!(flag_after, false); } diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index f03af703e..77c236d9c 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -44,6 +44,7 @@ use std::time::{SystemTime}; use futures::future::result; use time::format_description::parse; use time::OffsetDateTime; +use variant_count::VariantCount; use web3::types::H256; use crate::accountant::scanners::payable_scanner_extension::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; @@ -1284,7 +1285,7 @@ impl ReceivableScanner { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, VariantCount)] pub enum StartScanError { NothingToProcess, NoConsumingWalletFound, @@ -1374,7 +1375,7 @@ impl StartScanError { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum MTError { AutomaticScanConflict, UnnecessaryRequest { hint_opt: Option }, diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index b05a05bf4..6f032777e 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -1,5 +1,6 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use crate::accountant::scanners::StartScanError; use crate::accountant::{ Accountant, ResponseSkeleton, ScanForNewPayables, ScanForPendingPayables, ScanForReceivables, ScanForRetryPayables, @@ -9,16 +10,16 @@ use crate::sub_lib::utils::{ NotifyHandle, NotifyHandleReal, NotifyLaterHandle, NotifyLaterHandleReal, }; use actix::{Actor, Context, Handler}; +use masq_lib::messages::ScanType; use std::cell::RefCell; -use std::marker::PhantomData; -use std::rc::Rc; -use std::sync::{Arc, Mutex, RwLock}; +use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; pub struct ScanSchedulers { pub payable: PayableScanScheduler, pub pending_payable: SimplePeriodicalScanScheduler, pub receivable: SimplePeriodicalScanScheduler, + pub scan_schedule_hint_from_error_resolver: Box, pub automatic_scans_enabled: bool, } @@ -30,6 +31,9 @@ impl ScanSchedulers { scan_intervals.pending_payable_scan_interval, ), receivable: SimplePeriodicalScanScheduler::new(scan_intervals.receivable_scan_interval), + scan_schedule_hint_from_error_resolver: Box::new( + ScanScheduleHintErrorResolverReal::default(), + ), automatic_scans_enabled, } } @@ -42,14 +46,15 @@ pub enum PayableScanSchedulerError { #[derive(Debug, PartialEq)] pub enum ScanScheduleHint { - Schedule, + Schedule(ScanType), DoNotSchedule, } -pub enum HintableScanners { +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum HintableScanner { NewPayables, RetryPayables, - PendingPayables, + PendingPayables { initial_pending_payable_scan: bool }, } pub struct PayableScanScheduler { @@ -193,13 +198,107 @@ where } } +// Scanners that conclude by scheduling a later scan (usually different from this one) must handle +// StartScanErrors carefully to maintain continuity and periodicity. Poor handling could disrupt +// the entire scan chain. Where possible, a different type of scan may be scheduled (avoiding +// repetition of the erroneous scan) to prevent a full panic, while ensuring no unresolved issues +// are left for future scans. A panic is justified only if the error is deemed impossible by design +// within the broader context of that location. +pub trait ScanScheduleHintErrorResolver { + fn resolve_hint_from_error( + &self, + hintable_scanner: HintableScanner, + error: &StartScanError, + is_externally_triggered: bool, + ) -> ScanScheduleHint; +} + +#[derive(Default)] +pub struct ScanScheduleHintErrorResolverReal {} + +impl ScanScheduleHintErrorResolver for ScanScheduleHintErrorResolverReal { + fn resolve_hint_from_error( + &self, + hintable_scanner: HintableScanner, + error: &StartScanError, + is_externally_triggered: bool, + ) -> ScanScheduleHint { + match hintable_scanner { + HintableScanner::NewPayables => { + Self::resolve_new_payables(error, is_externally_triggered) + } + HintableScanner::RetryPayables => { + Self::resolve_retry_payables(error, is_externally_triggered) + } + HintableScanner::PendingPayables { + initial_pending_payable_scan, + } => Self::resolve_pending_payables( + error, + initial_pending_payable_scan, + is_externally_triggered, + ), + } + } +} + +impl ScanScheduleHintErrorResolverReal { + fn resolve_new_payables(e: &StartScanError, is_externally_triggered: bool) -> ScanScheduleHint { + if is_externally_triggered { + todo!("do not schedule") + } else if matches!(e, StartScanError::ScanAlreadyRunning { .. }) { + todo!("unreachable") + } else { + todo!("schedule NewPayable") + } + } + + // This looks paradoxical, but this scanner should be shielded by the scanner positioned before + // it, which should not request this one if there was already something wrong at the beginning. + // We can be strict. + fn resolve_retry_payables( + e: &StartScanError, + is_externally_triggered: bool, + ) -> ScanScheduleHint { + if is_externally_triggered { + todo!("do not schedule") + } else { + todo!("unreachable") //Severe but true + } + } + + fn resolve_pending_payables( + e: &StartScanError, + initial_pending_payable_scan: bool, + is_externally_triggered: bool, + ) -> ScanScheduleHint { + if is_externally_triggered { + todo!("do not schedule") + } else if e == &StartScanError::NothingToProcess { + if initial_pending_payable_scan { + todo!("schedule NewPayable") + } else { + todo!("unreachable") + } + } else if e == &StartScanError::NoConsumingWalletFound { + todo!("schedule NewPayable") + } else { + todo!("unreachable") //TODO Severe, but it stands true (make sure the test has the enum variant check) + } + } +} + #[cfg(test)] mod tests { use crate::accountant::scanners::scan_schedulers::{ - NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, - ScanSchedulers, + HintableScanner, NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, + ScanScheduleHint, ScanSchedulers, }; + use crate::accountant::scanners::{MTError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; + use itertools::Itertools; + use lazy_static::lazy_static; + use masq_lib::messages::ScanType; + use std::panic::{catch_unwind, AssertUnwindSafe}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[test] @@ -357,4 +456,286 @@ mod tests { Duration::from_secs(32), ); } + + lazy_static! { + static ref ALL_START_SCAN_ERRORS: Vec = { + + let candidates = vec![ + StartScanError::NothingToProcess, + StartScanError::NoConsumingWalletFound, + StartScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Payables, started_at: SystemTime::now()}, + StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), + StartScanError::CalledFromNullScanner + ]; + + + let mut check_vec = candidates + .iter() + .fold(vec![],|mut acc, current|{ + acc.push(AllStartScanErrorsAdjustable::number_variant(current)); + acc + }); + // Making sure we didn't count in one variant multiple times + check_vec.dedup(); + assert_eq!(check_vec.len(), StartScanError::VARIANT_COUNT, "Check on variant exhaustiveness failed."); + candidates + }; + } + + struct AllStartScanErrorsAdjustable<'a> { + errors: Vec<&'a StartScanError>, + } + + impl<'a> Default for AllStartScanErrorsAdjustable<'a> { + fn default() -> Self { + Self { + errors: ALL_START_SCAN_ERRORS.iter().collect_vec(), + } + } + } + + impl<'a> AllStartScanErrorsAdjustable<'a> { + fn eliminate_already_tested_variants( + mut self, + errors_to_eliminate: Vec, + ) -> Self { + let original_errors_tuples = self + .errors + .iter() + .map(|err| (Self::number_variant(*err), err)) + .collect_vec(); + let errors_to_eliminate_num_rep = errors_to_eliminate + .iter() + .map(Self::number_variant) + .collect_vec(); + let adjusted = errors_to_eliminate_num_rep + .into_iter() + .fold(original_errors_tuples, |acc, current| { + acc.into_iter() + .filter(|(num, _)| num != ¤t) + .collect_vec() + }) + .into_iter() + .map(|(_, err)| *err) + .collect_vec(); + self.errors = adjusted; + self + } + + fn number_variant(error: &StartScanError) -> usize { + match error { + StartScanError::NothingToProcess => 1, + StartScanError::NoConsumingWalletFound => 2, + StartScanError::ScanAlreadyRunning { .. } => 3, + StartScanError::CalledFromNullScanner => 4, + StartScanError::ManualTriggerError(..) => 5, + } + } + } + + #[test] + fn resolve_hint_from_error_works_for_pending_payables_if_externally_triggered() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + test_what_if_externally_triggered( + &subject, + HintableScanner::PendingPayables { + initial_pending_payable_scan: false, + }, + ); + test_what_if_externally_triggered( + &subject, + HintableScanner::PendingPayables { + initial_pending_payable_scan: true, + }, + ); + } + + fn test_what_if_externally_triggered( + subject: &ScanSchedulers, + hintable_scanner: HintableScanner, + ) { + ALL_START_SCAN_ERRORS + .iter() + .enumerate() + .for_each(|(idx, (error))| { + let result = subject + .scan_schedule_hint_from_error_resolver + .resolve_hint_from_error(hintable_scanner, error, true); + + assert_eq!( + result, + ScanScheduleHint::DoNotSchedule, + "We expected DoNotSchedule but got {:?} at idx {} for {:?}", + result, + idx, + hintable_scanner + ); + }) + } + + #[test] + fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true( + ) { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + let result = subject + .scan_schedule_hint_from_error_resolver + .resolve_hint_from_error( + HintableScanner::PendingPayables { + initial_pending_payable_scan: true, + }, + &StartScanError::NothingToProcess, + false, + ); + + assert_eq!( + result, + ScanScheduleHint::Schedule(ScanType::Payables), + "We expected Schedule(Payables) but got {:?}", + result, + ); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: an automatic pending payable scan should always be scheduled only if needed, which contradicts StartScanError::NothingToProcess" + )] + fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_false( + ) { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + let _ = subject + .scan_schedule_hint_from_error_resolver + .resolve_hint_from_error( + HintableScanner::PendingPayables { + initial_pending_payable_scan: false, + }, + &StartScanError::NothingToProcess, + false, + ); + } + + #[test] + fn resolve_error_for_pending_payables_if_no_consuming_wallet_found() { + fn test_no_consuming_wallet_found( + subject: &ScanSchedulers, + hintable_scanner: HintableScanner, + ) { + let result = subject + .scan_schedule_hint_from_error_resolver + .resolve_hint_from_error( + hintable_scanner, + &StartScanError::NoConsumingWalletFound, + false, + ); + + assert_eq!( + result, + ScanScheduleHint::Schedule(ScanType::Payables), + "We expected Schedule(Payables) but got {:?} for {:?}", + result, + hintable_scanner + ); + } + + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + test_no_consuming_wallet_found( + &subject, + HintableScanner::PendingPayables { + initial_pending_payable_scan: false, + }, + ); + test_no_consuming_wallet_found( + &subject, + HintableScanner::PendingPayables { + initial_pending_payable_scan: true, + }, + ); + } + + #[test] + fn resolve_error_for_pending_payables_forbidden_states() { + fn test_forbidden_states( + subject: &ScanSchedulers, + inputs: &AllStartScanErrorsAdjustable, + initial_pending_payable_scan: bool, + ) { + inputs.errors.iter().for_each(|error|{ + let panic = catch_unwind(AssertUnwindSafe(|| subject + .scan_schedule_hint_from_error_resolver + .resolve_hint_from_error(HintableScanner::PendingPayables {initial_pending_payable_scan},*error, false))).unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let expected_msg = format!("internal error: entered unreachable code: {:?} is not acceptable given an automatic scan of the PendingPayableScanner", error); + assert_eq!( + panic_msg, + &expected_msg, + "We expected '{}' but got '{}' for initial_pending_payable_scan = {}", + expected_msg, + panic_msg, + initial_pending_payable_scan + ) + }) + } + + let inputs = + AllStartScanErrorsAdjustable::default().eliminate_already_tested_variants(vec![ + StartScanError::NothingToProcess, + StartScanError::NoConsumingWalletFound, + ]); + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + test_forbidden_states(&subject, &inputs, false); + test_forbidden_states(&subject, &inputs, true); + } } + +// +// fn resolve_pending_payables( +// e: StartScanError, +// initial_pending_payable_scan: bool, +// is_externally_triggered: bool, +// ) -> ScanScheduleHint { +// if is_externally_triggered { +// todo!("do not schedule") +// } else if e == StartScanError::NothingToProcess { +// if initial_pending_payable_scan { +// todo!("schedule NewPayable") +// } else { +// todo!("unreachable") +// } +// } else if e == StartScanError::NoConsumingWalletFound { +// todo!("schedule NewPayable") +// } else { +// todo!("unreachable") //TODO Severe, but it stands true (make sure the test has the enum variant check) +// } +// } +// } + +// +// impl ScanScheduleHintErrorResolverReal { +// fn resolve_new_payables(e: StartScanError, is_externally_triggered: bool) -> ScanScheduleHint { +// if is_externally_triggered { +// todo!("do not schedule") +// } else if matches!(e, StartScanError::ScanAlreadyRunning { .. }) { +// todo!("unreachable") +// } else { +// todo!("schedule NewPayable") +// } +// } +// +// // This looks paradoxical, but this scanner should be shielded by the scanner positioned before +// // it, which should not request this one if there was already something wrong at the beginning. +// // We can be strict. +// fn resolve_retry_payables( +// e: StartScanError, +// is_externally_triggered: bool, +// ) -> ScanScheduleHint { +// if is_externally_triggered { +// todo!("do not schedule") +// } else { +// todo!("unreachable") //Severe but true +// } +// } From 01d5091c78070fd10c6b3ad0cb7cd8c001d1cb18 Mon Sep 17 00:00:00 2001 From: Bert Date: Tue, 20 May 2025 17:29:40 +0200 Subject: [PATCH 23/49] GH-602: all tests in scan_schedulers passing now --- .../accountant/scanners/scan_schedulers.rs | 228 +++++++++++------- 1 file changed, 147 insertions(+), 81 deletions(-) diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 6f032777e..9a1ed0a78 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -242,47 +242,63 @@ impl ScanScheduleHintErrorResolver for ScanScheduleHintErrorResolverReal { } impl ScanScheduleHintErrorResolverReal { - fn resolve_new_payables(e: &StartScanError, is_externally_triggered: bool) -> ScanScheduleHint { + fn resolve_new_payables( + err: &StartScanError, + is_externally_triggered: bool, + ) -> ScanScheduleHint { if is_externally_triggered { - todo!("do not schedule") - } else if matches!(e, StartScanError::ScanAlreadyRunning { .. }) { - todo!("unreachable") + ScanScheduleHint::DoNotSchedule + } else if matches!(err, StartScanError::ScanAlreadyRunning { .. }) { + unreachable!( + "an automatic scan of NewPayableScanner should never interfere with \ + itself {:?}", + err + ) } else { - todo!("schedule NewPayable") + ScanScheduleHint::Schedule(ScanType::Payables) } } - // This looks paradoxical, but this scanner should be shielded by the scanner positioned before - // it, which should not request this one if there was already something wrong at the beginning. - // We can be strict. + // This looks paradoxical, but this scanner is meant to be shielded by the scanner right before + // it. That should ensure this scanner will not be requested if there was already something + // fishy. We can go strict. fn resolve_retry_payables( - e: &StartScanError, + err: &StartScanError, is_externally_triggered: bool, ) -> ScanScheduleHint { if is_externally_triggered { - todo!("do not schedule") + ScanScheduleHint::DoNotSchedule } else { - todo!("unreachable") //Severe but true + unreachable!( + "{:?} is not acceptable for an automatic scan of RetryPayablesScanner", + err + ) } } fn resolve_pending_payables( - e: &StartScanError, + err: &StartScanError, initial_pending_payable_scan: bool, is_externally_triggered: bool, ) -> ScanScheduleHint { if is_externally_triggered { - todo!("do not schedule") - } else if e == &StartScanError::NothingToProcess { + ScanScheduleHint::DoNotSchedule + } else if err == &StartScanError::NothingToProcess { if initial_pending_payable_scan { - todo!("schedule NewPayable") + ScanScheduleHint::Schedule(ScanType::Payables) } else { - todo!("unreachable") + unreachable!( + "the automatic pending payable scan should always be requested only in need, \ + which contradicts the current StartScanError::NothingToProcess" + ) } - } else if e == &StartScanError::NoConsumingWalletFound { - todo!("schedule NewPayable") + } else if err == &StartScanError::NoConsumingWalletFound { + ScanScheduleHint::Schedule(ScanType::Payables) } else { - todo!("unreachable") //TODO Severe, but it stands true (make sure the test has the enum variant check) + unreachable!( + "{:?} is not acceptable for an automatic scan of PendingPayableScanner", + err + ) } } } @@ -441,8 +457,8 @@ mod tests { #[test] #[should_panic( - expected = "Unexpected now (SystemTime { tv_sec: 999999, tv_nsec: 0 }) earlier \ - than past timestamp (SystemTime { tv_sec: 1000000, tv_nsec: 0 })" + expected = "Unexpected now (SystemTime { tv_sec: 999999, tv_nsec: 0 }) earlier than past \ + timestamp (SystemTime { tv_sec: 1000000, tv_nsec: 0 })" )] fn scan_dyn_interval_computer_panics() { let now = UNIX_EPOCH @@ -477,7 +493,8 @@ mod tests { }); // Making sure we didn't count in one variant multiple times check_vec.dedup(); - assert_eq!(check_vec.len(), StartScanError::VARIANT_COUNT, "Check on variant exhaustiveness failed."); + assert_eq!(check_vec.len(), StartScanError::VARIANT_COUNT, "Check on variant \ + exhaustiveness failed."); candidates }; } @@ -599,7 +616,9 @@ mod tests { #[test] #[should_panic( - expected = "internal error: entered unreachable code: an automatic pending payable scan should always be scheduled only if needed, which contradicts StartScanError::NothingToProcess" + expected = "internal error: entered unreachable code: the automatic pending payable scan \ + should always be requested only in need, which contradicts the current \ + StartScanError::NothingToProcess" )] fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_false( ) { @@ -662,20 +681,30 @@ mod tests { inputs: &AllStartScanErrorsAdjustable, initial_pending_payable_scan: bool, ) { - inputs.errors.iter().for_each(|error|{ - let panic = catch_unwind(AssertUnwindSafe(|| subject - .scan_schedule_hint_from_error_resolver - .resolve_hint_from_error(HintableScanner::PendingPayables {initial_pending_payable_scan},*error, false))).unwrap_err(); + inputs.errors.iter().for_each(|error| { + let panic = catch_unwind(AssertUnwindSafe(|| { + subject + .scan_schedule_hint_from_error_resolver + .resolve_hint_from_error( + HintableScanner::PendingPayables { + initial_pending_payable_scan, + }, + *error, + false, + ) + })) + .unwrap_err(); let panic_msg = panic.downcast_ref::().unwrap(); - let expected_msg = format!("internal error: entered unreachable code: {:?} is not acceptable given an automatic scan of the PendingPayableScanner", error); + let expected_msg = format!( + "internal error: entered unreachable code: {:?} is not acceptable for \ + an automatic scan of PendingPayableScanner", + error + ); assert_eq!( - panic_msg, - &expected_msg, + panic_msg, &expected_msg, "We expected '{}' but got '{}' for initial_pending_payable_scan = {}", - expected_msg, - panic_msg, - initial_pending_payable_scan + expected_msg, panic_msg, initial_pending_payable_scan ) }) } @@ -690,52 +719,89 @@ mod tests { test_forbidden_states(&subject, &inputs, false); test_forbidden_states(&subject, &inputs, true); } -} -// -// fn resolve_pending_payables( -// e: StartScanError, -// initial_pending_payable_scan: bool, -// is_externally_triggered: bool, -// ) -> ScanScheduleHint { -// if is_externally_triggered { -// todo!("do not schedule") -// } else if e == StartScanError::NothingToProcess { -// if initial_pending_payable_scan { -// todo!("schedule NewPayable") -// } else { -// todo!("unreachable") -// } -// } else if e == StartScanError::NoConsumingWalletFound { -// todo!("schedule NewPayable") -// } else { -// todo!("unreachable") //TODO Severe, but it stands true (make sure the test has the enum variant check) -// } -// } -// } - -// -// impl ScanScheduleHintErrorResolverReal { -// fn resolve_new_payables(e: StartScanError, is_externally_triggered: bool) -> ScanScheduleHint { -// if is_externally_triggered { -// todo!("do not schedule") -// } else if matches!(e, StartScanError::ScanAlreadyRunning { .. }) { -// todo!("unreachable") -// } else { -// todo!("schedule NewPayable") -// } -// } -// -// // This looks paradoxical, but this scanner should be shielded by the scanner positioned before -// // it, which should not request this one if there was already something wrong at the beginning. -// // We can be strict. -// fn resolve_retry_payables( -// e: StartScanError, -// is_externally_triggered: bool, -// ) -> ScanScheduleHint { -// if is_externally_triggered { -// todo!("do not schedule") -// } else { -// todo!("unreachable") //Severe but true -// } -// } + #[test] + fn resolve_hint_from_error_works_for_retry_payables_if_externally_triggered() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + test_what_if_externally_triggered(&subject, HintableScanner::RetryPayables {}); + } + + #[test] + fn any_automatic_scan_with_start_scan_error_is_fatal_for_retry_payables() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + ALL_START_SCAN_ERRORS.iter().for_each(|error| { + let panic = catch_unwind(AssertUnwindSafe(|| { + subject + .scan_schedule_hint_from_error_resolver + .resolve_hint_from_error(HintableScanner::RetryPayables, error, false) + })) + .unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let expected_msg = format!( + "internal error: entered unreachable code: {:?} is not acceptable for an automatic \ + scan of RetryPayablesScanner", + error + ); + assert_eq!( + panic_msg, &expected_msg, + "We expected '{}' but got '{}'", + expected_msg, panic_msg, + ) + }) + } + + #[test] + fn resolve_hint_from_error_works_for_new_payables_if_externally_triggered() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + test_what_if_externally_triggered(&subject, HintableScanner::NewPayables {}); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: an automatic scan of NewPayableScanner \ + should never interfere with itself ScanAlreadyRunning { pertinent_scanner: Payables, started_at:" + )] + fn resolve_hint_for_new_payables_if_scan_is_already_running_err_and_is_automatic_scan() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + let _ = subject + .scan_schedule_hint_from_error_resolver + .resolve_hint_from_error( + HintableScanner::NewPayables, + &StartScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Payables, + started_at: SystemTime::now(), + }, + false, + ); + } + + #[test] + fn resolve_hint_for_new_payables_with_those_cases_that_result_in_future_rescheduling() { + let inputs = + AllStartScanErrorsAdjustable::default().eliminate_already_tested_variants(vec![ + StartScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Payables, + started_at: SystemTime::now(), + }, + ]); + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + inputs.errors.iter().for_each(|error| { + let result = subject + .scan_schedule_hint_from_error_resolver + .resolve_hint_from_error(HintableScanner::NewPayables, *error, false); + + assert_eq!( + result, + ScanScheduleHint::Schedule(ScanType::Payables), + "We expected Schedule(Payables) but got '{:?}'", + result, + ) + }) + } +} From 36afdf3dca94ac95ec0d54a3fff83a0f6ff33513 Mon Sep 17 00:00:00 2001 From: Bert Date: Tue, 20 May 2025 17:29:40 +0200 Subject: [PATCH 24/49] GH-602: all tests in scan_schedulers passing now --- node/src/accountant/mod.rs | 12 +- .../accountant/scanners/scan_schedulers.rs | 264 +++++++++++------- 2 files changed, 171 insertions(+), 105 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index f578e44e7..3933ba7f6 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -943,8 +943,8 @@ impl Accountant { }); self.scan_schedulers - .scan_schedule_hint_from_error_resolver - .resolve_hint_from_error(todo!(), &e, response_skeleton_opt.is_some()) + .schedule_hint_on_error_resolver + .resolve_hint_for_given_error(todo!(), &e, response_skeleton_opt.is_some()) } } } @@ -993,8 +993,8 @@ impl Accountant { // }); self.scan_schedulers - .scan_schedule_hint_from_error_resolver - .resolve_hint_from_error(todo!(), &e, response_skeleton_opt.is_some()) + .schedule_hint_on_error_resolver + .resolve_hint_for_given_error(todo!(), &e, response_skeleton_opt.is_some()) } } } @@ -1043,8 +1043,8 @@ impl Accountant { }); self.scan_schedulers - .scan_schedule_hint_from_error_resolver - .resolve_hint_from_error(todo!(), &e, response_skeleton_opt.is_some()) + .schedule_hint_on_error_resolver + .resolve_hint_for_given_error(todo!(), &e, response_skeleton_opt.is_some()) } }; diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 6f032777e..d58a57052 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -19,7 +19,7 @@ pub struct ScanSchedulers { pub payable: PayableScanScheduler, pub pending_payable: SimplePeriodicalScanScheduler, pub receivable: SimplePeriodicalScanScheduler, - pub scan_schedule_hint_from_error_resolver: Box, + pub schedule_hint_on_error_resolver: Box, pub automatic_scans_enabled: bool, } @@ -31,8 +31,8 @@ impl ScanSchedulers { scan_intervals.pending_payable_scan_interval, ), receivable: SimplePeriodicalScanScheduler::new(scan_intervals.receivable_scan_interval), - scan_schedule_hint_from_error_resolver: Box::new( - ScanScheduleHintErrorResolverReal::default(), + schedule_hint_on_error_resolver: Box::new( + ScheduleHintOnErrorResolverReal::default(), ), automatic_scans_enabled, } @@ -204,8 +204,8 @@ where // repetition of the erroneous scan) to prevent a full panic, while ensuring no unresolved issues // are left for future scans. A panic is justified only if the error is deemed impossible by design // within the broader context of that location. -pub trait ScanScheduleHintErrorResolver { - fn resolve_hint_from_error( +pub trait ScheduleHintOnErrorResolver { + fn resolve_hint_for_given_error( &self, hintable_scanner: HintableScanner, error: &StartScanError, @@ -214,10 +214,10 @@ pub trait ScanScheduleHintErrorResolver { } #[derive(Default)] -pub struct ScanScheduleHintErrorResolverReal {} +pub struct ScheduleHintOnErrorResolverReal {} -impl ScanScheduleHintErrorResolver for ScanScheduleHintErrorResolverReal { - fn resolve_hint_from_error( +impl ScheduleHintOnErrorResolver for ScheduleHintOnErrorResolverReal { + fn resolve_hint_for_given_error( &self, hintable_scanner: HintableScanner, error: &StartScanError, @@ -241,48 +241,64 @@ impl ScanScheduleHintErrorResolver for ScanScheduleHintErrorResolverReal { } } -impl ScanScheduleHintErrorResolverReal { - fn resolve_new_payables(e: &StartScanError, is_externally_triggered: bool) -> ScanScheduleHint { +impl ScheduleHintOnErrorResolverReal { + fn resolve_new_payables( + err: &StartScanError, + is_externally_triggered: bool, + ) -> ScanScheduleHint { if is_externally_triggered { - todo!("do not schedule") - } else if matches!(e, StartScanError::ScanAlreadyRunning { .. }) { - todo!("unreachable") + ScanScheduleHint::DoNotSchedule + } else if matches!(err, StartScanError::ScanAlreadyRunning { .. }) { + unreachable!( + "an automatic scan of NewPayableScanner should never interfere with \ + itself {:?}", + err + ) } else { - todo!("schedule NewPayable") + ScanScheduleHint::Schedule(ScanType::Payables) } } - // This looks paradoxical, but this scanner should be shielded by the scanner positioned before - // it, which should not request this one if there was already something wrong at the beginning. - // We can be strict. + // This looks paradoxical, but this scanner is meant to be shielded by the scanner right before + // it. That should ensure this scanner will not be requested if there was already something + // fishy. We can go strict. fn resolve_retry_payables( - e: &StartScanError, + err: &StartScanError, is_externally_triggered: bool, ) -> ScanScheduleHint { if is_externally_triggered { - todo!("do not schedule") + ScanScheduleHint::DoNotSchedule } else { - todo!("unreachable") //Severe but true + unreachable!( + "{:?} is not acceptable for an automatic scan of RetryPayablesScanner", + err + ) } } fn resolve_pending_payables( - e: &StartScanError, + err: &StartScanError, initial_pending_payable_scan: bool, is_externally_triggered: bool, ) -> ScanScheduleHint { if is_externally_triggered { - todo!("do not schedule") - } else if e == &StartScanError::NothingToProcess { + ScanScheduleHint::DoNotSchedule + } else if err == &StartScanError::NothingToProcess { if initial_pending_payable_scan { - todo!("schedule NewPayable") + ScanScheduleHint::Schedule(ScanType::Payables) } else { - todo!("unreachable") + unreachable!( + "the automatic pending payable scan should always be requested only in need, \ + which contradicts the current StartScanError::NothingToProcess" + ) } - } else if e == &StartScanError::NoConsumingWalletFound { - todo!("schedule NewPayable") + } else if err == &StartScanError::NoConsumingWalletFound { + ScanScheduleHint::Schedule(ScanType::Payables) } else { - todo!("unreachable") //TODO Severe, but it stands true (make sure the test has the enum variant check) + unreachable!( + "{:?} is not acceptable for an automatic scan of PendingPayableScanner", + err + ) } } } @@ -441,8 +457,8 @@ mod tests { #[test] #[should_panic( - expected = "Unexpected now (SystemTime { tv_sec: 999999, tv_nsec: 0 }) earlier \ - than past timestamp (SystemTime { tv_sec: 1000000, tv_nsec: 0 })" + expected = "Unexpected now (SystemTime { tv_sec: 999999, tv_nsec: 0 }) earlier than past \ + timestamp (SystemTime { tv_sec: 1000000, tv_nsec: 0 })" )] fn scan_dyn_interval_computer_panics() { let now = UNIX_EPOCH @@ -477,7 +493,8 @@ mod tests { }); // Making sure we didn't count in one variant multiple times check_vec.dedup(); - assert_eq!(check_vec.len(), StartScanError::VARIANT_COUNT, "Check on variant exhaustiveness failed."); + assert_eq!(check_vec.len(), StartScanError::VARIANT_COUNT, "Check on variant \ + exhaustiveness failed."); candidates }; } @@ -534,7 +551,7 @@ mod tests { } #[test] - fn resolve_hint_from_error_works_for_pending_payables_if_externally_triggered() { + fn resolve_hint_for_given_error_works_for_pending_payables_if_externally_triggered() { let subject = ScanSchedulers::new(ScanIntervals::default(), true); test_what_if_externally_triggered( @@ -560,8 +577,8 @@ mod tests { .enumerate() .for_each(|(idx, (error))| { let result = subject - .scan_schedule_hint_from_error_resolver - .resolve_hint_from_error(hintable_scanner, error, true); + .schedule_hint_on_error_resolver + .resolve_hint_for_given_error(hintable_scanner, error, true); assert_eq!( result, @@ -580,8 +597,8 @@ mod tests { let subject = ScanSchedulers::new(ScanIntervals::default(), true); let result = subject - .scan_schedule_hint_from_error_resolver - .resolve_hint_from_error( + .schedule_hint_on_error_resolver + .resolve_hint_for_given_error( HintableScanner::PendingPayables { initial_pending_payable_scan: true, }, @@ -599,15 +616,17 @@ mod tests { #[test] #[should_panic( - expected = "internal error: entered unreachable code: an automatic pending payable scan should always be scheduled only if needed, which contradicts StartScanError::NothingToProcess" + expected = "internal error: entered unreachable code: the automatic pending payable scan \ + should always be requested only in need, which contradicts the current \ + StartScanError::NothingToProcess" )] fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_false( ) { let subject = ScanSchedulers::new(ScanIntervals::default(), true); let _ = subject - .scan_schedule_hint_from_error_resolver - .resolve_hint_from_error( + .schedule_hint_on_error_resolver + .resolve_hint_for_given_error( HintableScanner::PendingPayables { initial_pending_payable_scan: false, }, @@ -623,8 +642,8 @@ mod tests { hintable_scanner: HintableScanner, ) { let result = subject - .scan_schedule_hint_from_error_resolver - .resolve_hint_from_error( + .schedule_hint_on_error_resolver + .resolve_hint_for_given_error( hintable_scanner, &StartScanError::NoConsumingWalletFound, false, @@ -662,20 +681,30 @@ mod tests { inputs: &AllStartScanErrorsAdjustable, initial_pending_payable_scan: bool, ) { - inputs.errors.iter().for_each(|error|{ - let panic = catch_unwind(AssertUnwindSafe(|| subject - .scan_schedule_hint_from_error_resolver - .resolve_hint_from_error(HintableScanner::PendingPayables {initial_pending_payable_scan},*error, false))).unwrap_err(); + inputs.errors.iter().for_each(|error| { + let panic = catch_unwind(AssertUnwindSafe(|| { + subject + .schedule_hint_on_error_resolver + .resolve_hint_for_given_error( + HintableScanner::PendingPayables { + initial_pending_payable_scan, + }, + *error, + false, + ) + })) + .unwrap_err(); let panic_msg = panic.downcast_ref::().unwrap(); - let expected_msg = format!("internal error: entered unreachable code: {:?} is not acceptable given an automatic scan of the PendingPayableScanner", error); + let expected_msg = format!( + "internal error: entered unreachable code: {:?} is not acceptable for \ + an automatic scan of PendingPayableScanner", + error + ); assert_eq!( - panic_msg, - &expected_msg, + panic_msg, &expected_msg, "We expected '{}' but got '{}' for initial_pending_payable_scan = {}", - expected_msg, - panic_msg, - initial_pending_payable_scan + expected_msg, panic_msg, initial_pending_payable_scan ) }) } @@ -690,52 +719,89 @@ mod tests { test_forbidden_states(&subject, &inputs, false); test_forbidden_states(&subject, &inputs, true); } -} -// -// fn resolve_pending_payables( -// e: StartScanError, -// initial_pending_payable_scan: bool, -// is_externally_triggered: bool, -// ) -> ScanScheduleHint { -// if is_externally_triggered { -// todo!("do not schedule") -// } else if e == StartScanError::NothingToProcess { -// if initial_pending_payable_scan { -// todo!("schedule NewPayable") -// } else { -// todo!("unreachable") -// } -// } else if e == StartScanError::NoConsumingWalletFound { -// todo!("schedule NewPayable") -// } else { -// todo!("unreachable") //TODO Severe, but it stands true (make sure the test has the enum variant check) -// } -// } -// } - -// -// impl ScanScheduleHintErrorResolverReal { -// fn resolve_new_payables(e: StartScanError, is_externally_triggered: bool) -> ScanScheduleHint { -// if is_externally_triggered { -// todo!("do not schedule") -// } else if matches!(e, StartScanError::ScanAlreadyRunning { .. }) { -// todo!("unreachable") -// } else { -// todo!("schedule NewPayable") -// } -// } -// -// // This looks paradoxical, but this scanner should be shielded by the scanner positioned before -// // it, which should not request this one if there was already something wrong at the beginning. -// // We can be strict. -// fn resolve_retry_payables( -// e: StartScanError, -// is_externally_triggered: bool, -// ) -> ScanScheduleHint { -// if is_externally_triggered { -// todo!("do not schedule") -// } else { -// todo!("unreachable") //Severe but true -// } -// } + #[test] + fn resolve_hint_for_given_error_works_for_retry_payables_if_externally_triggered() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + test_what_if_externally_triggered(&subject, HintableScanner::RetryPayables {}); + } + + #[test] + fn any_automatic_scan_with_start_scan_error_is_fatal_for_retry_payables() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + ALL_START_SCAN_ERRORS.iter().for_each(|error| { + let panic = catch_unwind(AssertUnwindSafe(|| { + subject + .schedule_hint_on_error_resolver + .resolve_hint_for_given_error(HintableScanner::RetryPayables, error, false) + })) + .unwrap_err(); + + let panic_msg = panic.downcast_ref::().unwrap(); + let expected_msg = format!( + "internal error: entered unreachable code: {:?} is not acceptable for an automatic \ + scan of RetryPayablesScanner", + error + ); + assert_eq!( + panic_msg, &expected_msg, + "We expected '{}' but got '{}'", + expected_msg, panic_msg, + ) + }) + } + + #[test] + fn resolve_hint_for_given_error_works_for_new_payables_if_externally_triggered() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + test_what_if_externally_triggered(&subject, HintableScanner::NewPayables {}); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: an automatic scan of NewPayableScanner \ + should never interfere with itself ScanAlreadyRunning { pertinent_scanner: Payables, started_at:" + )] + fn resolve_hint_for_new_payables_if_scan_is_already_running_error_and_is_automatic_scan() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + let _ = subject + .schedule_hint_on_error_resolver + .resolve_hint_for_given_error( + HintableScanner::NewPayables, + &StartScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Payables, + started_at: SystemTime::now(), + }, + false, + ); + } + + #[test] + fn resolve_hint_for_new_payables_with_those_error_cases_that_result_in_future_rescheduling() { + let inputs = + AllStartScanErrorsAdjustable::default().eliminate_already_tested_variants(vec![ + StartScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Payables, + started_at: SystemTime::now(), + }, + ]); + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + + inputs.errors.iter().for_each(|error| { + let result = subject + .schedule_hint_on_error_resolver + .resolve_hint_for_given_error(HintableScanner::NewPayables, *error, false); + + assert_eq!( + result, + ScanScheduleHint::Schedule(ScanType::Payables), + "We expected Schedule(Payables) but got '{:?}'", + result, + ) + }) + } +} From 11b2e15cbc19c3b9e0957ad2f1205f1f8c9099da Mon Sep 17 00:00:00 2001 From: Bert Date: Tue, 20 May 2025 23:46:53 +0200 Subject: [PATCH 25/49] GH-602: slowly fixing test in accountant/mod.rs --- node/src/accountant/mod.rs | 191 +++++++++--------- node/src/accountant/scanners/mod.rs | 52 ++--- .../accountant/scanners/scan_schedulers.rs | 41 +++- node/src/test_utils/mod.rs | 2 +- 4 files changed, 146 insertions(+), 140 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 3933ba7f6..bba4a1eef 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -77,7 +77,8 @@ use std::rc::Rc; use std::str::RMatches; use std::time::SystemTime; use web3::types::H256; -use crate::accountant::scanners::scan_schedulers::{ScanScheduleHint, ScanSchedulers}; +use crate::accountant::scanners::scan_schedulers::{HintableScanner, ScanScheduleHint, ScanSchedulers}; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; @@ -228,10 +229,7 @@ impl Handler for Accountant { if let ScanScheduleHint::Schedule(ScanType::Payables) = self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) { - todo!() - // self.scan_schedulers - // .pending_payable - // .schedule(ctx, response_skeleton_opt); + self.scan_schedulers.payable.schedule_for_new_payable(ctx); } } } @@ -323,14 +321,13 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: SentPayables, ctx: &mut Self::Context) -> Self::Result { - match self.scanners.finish_payable_scan(msg, &self.logger) { - None => { - todo!("Finishing automatic payable scan") //self.scan_schedulers.pending_payable.schedule(ctx, None) - } - // TODO Might be worth a consideration with GH-635 - // Some(node_to_ui_msg) if self.scan_schedulers.automatic_scans_enabled => { - // todo!(); - // } + let scan_result = self.scanners.finish_payable_scan(msg, &self.logger); + + match scan_result.ui_response_opt { + None => match scan_result.result { + OperationOutcome::NewPendingPayable => todo!(), + OperationOutcome::Failure => todo!(), + }, Some(node_to_ui_msg) => { todo!("Externally triggered payable scan is finishing"); self.ui_message_sub_opt @@ -924,28 +921,11 @@ impl Accountant { todo!() // ScanScheduleHint::DoNotSchedule } - Err(e) => { - e.log_error( - &self.logger, - ScanType::Payables, - response_skeleton_opt.is_some(), - ); - - response_skeleton_opt.map(|skeleton| { - self.ui_message_sub_opt - .as_ref() - .expect("UiGateway is unbound") - .try_send(NodeToUiMessage { - target: MessageTarget::ClientId(skeleton.client_id), - body: UiScanResponse {}.tmb(skeleton.context_id), - }) - .expect("UiGateway is dead"); - }); - - self.scan_schedulers - .schedule_hint_on_error_resolver - .resolve_hint_for_given_error(todo!(), &e, response_skeleton_opt.is_some()) - } + Err(e) => self.handle_start_scan_error_for_scanner_with_irregular_scheduling( + HintableScanner::NewPayables, + e, + response_skeleton_opt, + ), } } @@ -971,30 +951,36 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - todo!() + ScanScheduleHint::DoNotSchedule } Err(e) => { - todo!("process...and eventually schedule for pending payable"); - // e.log_error( - // &self.logger, - // ScanType::Payables, - // response_skeleton_opt.is_some(), - // ); + self.handle_start_scan_error_for_scanner_with_irregular_scheduling( + todo!(), + e, + response_skeleton_opt, + ) + // let is_externally_triggered = response_skeleton_opt.is_some(); + // todo!("process...and eventually schedule for pending payable"); + // // e.log_error( + // // &self.logger, + // // ScanType::Payables, + // // is_externally_triggered, + // // ); + // // + // // response_skeleton_opt.map(|skeleton| { + // // self.ui_message_sub_opt + // // .as_ref() + // // .expect("UiGateway is unbound") + // // .try_send(NodeToUiMessage { + // // target: MessageTarget::ClientId(skeleton.client_id), + // // body: UiScanResponse {}.tmb(skeleton.context_id), + // // }) + // // .expect("UiGateway is dead"); + // // }); // - // response_skeleton_opt.map(|skeleton| { - // self.ui_message_sub_opt - // .as_ref() - // .expect("UiGateway is unbound") - // .try_send(NodeToUiMessage { - // target: MessageTarget::ClientId(skeleton.client_id), - // body: UiScanResponse {}.tmb(skeleton.context_id), - // }) - // .expect("UiGateway is dead"); - // }); - - self.scan_schedulers - .schedule_hint_on_error_resolver - .resolve_hint_for_given_error(todo!(), &e, response_skeleton_opt.is_some()) + // self.scan_schedulers + // .schedule_hint_on_error_resolver + // .resolve_hint_for_given_error(todo!(), &e, is_externally_triggered) } } } @@ -1025,37 +1011,54 @@ impl Accountant { todo!("give a hint") } Err(e) => { - e.log_error( - &self.logger, - ScanType::PendingPayables, - response_skeleton_opt.is_some(), - ); - - response_skeleton_opt.map(|skeleton| { - self.ui_message_sub_opt - .as_ref() - .expect("UiGateway is unbound") - .try_send(NodeToUiMessage { - target: MessageTarget::ClientId(skeleton.client_id), - body: UiScanResponse {}.tmb(skeleton.context_id), - }) - .expect("UiGateway is dead"); - }); - - self.scan_schedulers - .schedule_hint_on_error_resolver - .resolve_hint_for_given_error(todo!(), &e, response_skeleton_opt.is_some()) + let initial_pending_payable_scan = self.scanners.initial_pending_payable_scan(); + self.handle_start_scan_error_for_scanner_with_irregular_scheduling( + HintableScanner::PendingPayables { + initial_pending_payable_scan, + }, + e, + response_skeleton_opt, + ) } }; if self.scanners.initial_pending_payable_scan() { - todo!("set me right") - //self.scanners.unset_initial_pending_payable_scan() + self.scanners.unset_initial_pending_payable_scan() } hint } + fn handle_start_scan_error_for_scanner_with_irregular_scheduling( + &self, + hintable_scanner: HintableScanner, + e: StartScanError, + response_skeleton_opt: Option, + ) -> ScanScheduleHint { + let is_externally_triggered = response_skeleton_opt.is_some(); + + e.log_error( + &self.logger, + hintable_scanner.into(), + is_externally_triggered, + ); + + response_skeleton_opt.map(|skeleton| { + self.ui_message_sub_opt + .as_ref() + .expect("UiGateway is unbound") + .try_send(NodeToUiMessage { + target: MessageTarget::ClientId(skeleton.client_id), + body: UiScanResponse {}.tmb(skeleton.context_id), + }) + .expect("UiGateway is dead"); + }); + + self.scan_schedulers + .schedule_hint_on_error_resolver + .resolve_hint_for_given_error(hintable_scanner, &e, is_externally_triggered) + } + fn handle_request_of_scan_for_receivable( &mut self, response_skeleton_opt: Option, @@ -1910,8 +1913,8 @@ mod tests { scan_type: ScanType, ) { let expected_log_msg = format!( - "WARN: {test_name}: Manual {:?} scan was denied. Automatic scanning setup prevents \ - manual triggers.", + "WARN: {test_name}: Manual {:?} scan was denied. Automatic scanning \ + setup prevents manual triggers.", scan_type ); @@ -2507,13 +2510,18 @@ mod tests { NotifyHandleMock::default().notify_params(&scan_for_retry_payables_notify_params_arc), ); subject.scan_schedulers.payable.new_payable_notify = Box::new( - NotifyHandleMock::default().notify_params(&scan_for_new_payables_notify_params_arc), + NotifyHandleMock::default() + .capture_msg_and_let_it_fly_on() + .notify_params(&scan_for_new_payables_notify_params_arc), ); subject.scan_schedulers.receivable.handle = Box::new( NotifyLaterHandleMock::default() .notify_later_params(&scan_for_receivables_notify_later_params_arc) .stop_system_on_count_received(1), ); + // Important that this is short because the test relies on it to stop the system. + let receivable_scan_interval = Duration::from_millis(50); + subject.scan_schedulers.receivable.interval = receivable_scan_interval; let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() .compute_interval_params(&compute_interval_params_arc) .compute_interval_result(Some(new_payable_expected_computed_interval)); @@ -2617,19 +2625,7 @@ mod tests { ScanForReceivables { response_skeleton_opt: None }, - new_payable_expected_computed_interval - )] - ); - let scan_for_receivables_notify_later_params = - scan_for_receivables_notify_later_params_arc.lock().unwrap(); - let default_scan_intervals = ScanIntervals::default(); - assert_eq!( - *scan_for_receivables_notify_later_params, - vec![( - ScanForReceivables { - response_skeleton_opt: None - }, - default_scan_intervals.receivable_scan_interval + receivable_scan_interval )] ); // Finally, an assertion to prove course of actions in time. We expect the pending payable @@ -2637,13 +2633,6 @@ mod tests { // I do believe it's impossible that these two instants would happen at the same time, until // this is run on a real super-giga-computer. assert!(pp_scan_started_at < p_scheduling_now); - let tlh = TestLogHandler::new(); - tlh.exists_log_containing("INFO: Accountant: Scanning for pending payable"); - tlh.exists_log_containing(&format!( - "INFO: Accountant: Scanning for receivables to {}", - earning_wallet - )); - tlh.exists_log_containing("INFO: Accountant: Scanning for delinquencies"); } #[test] diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 77c236d9c..02b8d2981 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -37,6 +37,7 @@ use masq_lib::ui_gateway::{MessageTarget, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; use std::marker::PhantomData; use std::rc::Rc; use std::sync::{Arc, RwLock}; @@ -248,17 +249,13 @@ impl Scanners { .start_scan(wallet, timestamp, response_skeleton_opt, logger) } - pub fn finish_payable_scan( - &mut self, - msg: SentPayables, - logger: &Logger, - ) -> Option { + pub fn finish_payable_scan(&mut self, msg: SentPayables, logger: &Logger) -> PayableScanResult { let scan_result = self.payable.finish_scan(msg, logger); match scan_result.result { OperationOutcome::NewPendingPayable => self.aware_of_unresolved_pending_payable = true, OperationOutcome::Failure => (), }; - scan_result.ui_response_opt + scan_result } pub fn finish_pending_payable_scan( @@ -333,7 +330,6 @@ impl Scanners { MTError::AutomaticScanConflict, )) } else if self.initial_pending_payable_scan { - self.initial_pending_payable_scan = false; Ok(()) } else if triggered_manually && !self.aware_of_unresolved_pending_payable { Err(StartScanError::ManualTriggerError( @@ -1130,26 +1126,6 @@ impl StartableScanner for ReceivableSc } impl Scanner> for ReceivableScanner { - // fn start_scan( - // &mut self, - // earning_wallet: Wallet, - // timestamp: SystemTime, - // response_skeleton_opt: Option, - // logger: &Logger, - // ) -> Result { - // if let Some(timestamp) = self.scan_started_at() { - // return Err(StartScanError::ScanAlreadyRunning(timestamp)); - // } - // self.mark_as_started(timestamp); - // info!(logger, "Scanning for receivables to {}", earning_wallet); - // self.scan_for_delinquencies(timestamp, logger); - // - // Ok(RetrieveTransactions { - // recipient: earning_wallet, - // response_skeleton_opt, - // }) - // } - fn finish_scan(&mut self, msg: ReceivedPayments, logger: &Logger) -> Option { self.handle_new_received_payments(&msg, logger); self.mark_as_ended(logger); @@ -1400,8 +1376,6 @@ pub mod local_test_utils { use actix::{Message, System}; use itertools::Either; use masq_lib::logger::Logger; - use masq_lib::ui_gateway::NodeToUiMessage; - use rand::distributions::Standard; use std::any::type_name; use std::cell::RefCell; use std::sync::{Arc, Mutex}; @@ -2227,11 +2201,17 @@ mod tests { let aware_of_unresolved_pending_payable_before = subject.aware_of_unresolved_pending_payable; - let node_to_ui_msg = subject.finish_payable_scan(sent_payable, &logger); + let payable_scan_result = subject.finish_payable_scan(sent_payable, &logger); let is_scan_running = subject.scan_started_at(ScanType::Payables).is_some(); let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; - assert_eq!(node_to_ui_msg, None); + assert_eq!( + payable_scan_result, + PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::NewPendingPayable + } + ); assert_eq!(is_scan_running, false); assert_eq!(aware_of_unresolved_pending_payable_before, false); assert_eq!(aware_of_unresolved_pending_payable_after, true); @@ -2618,12 +2598,18 @@ mod tests { let aware_of_unresolved_pending_payable_before = subject.aware_of_unresolved_pending_payable; - let node_to_ui_msg_opt = subject.finish_payable_scan(sent_payable, &logger); + let payable_scan_result = subject.finish_payable_scan(sent_payable, &logger); let aware_of_unresolved_pending_payable_after = subject.aware_of_unresolved_pending_payable; System::current().stop(); system.run(); - assert_eq!(node_to_ui_msg_opt, None); + assert_eq!( + payable_scan_result, + PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::Failure + } + ); assert_eq!(aware_of_unresolved_pending_payable_before, false); assert_eq!(aware_of_unresolved_pending_payable_after, false); let fingerprints_rowids_params = fingerprints_rowids_params_arc.lock().unwrap(); diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index d58a57052..00cd149cb 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -31,9 +31,7 @@ impl ScanSchedulers { scan_intervals.pending_payable_scan_interval, ), receivable: SimplePeriodicalScanScheduler::new(scan_intervals.receivable_scan_interval), - schedule_hint_on_error_resolver: Box::new( - ScheduleHintOnErrorResolverReal::default(), - ), + schedule_hint_on_error_resolver: Box::new(ScheduleHintOnErrorResolverReal::default()), automatic_scans_enabled, } } @@ -57,6 +55,16 @@ pub enum HintableScanner { PendingPayables { initial_pending_payable_scan: bool }, } +impl From for ScanType { + fn from(hintable_scanner: HintableScanner) -> Self { + match hintable_scanner { + HintableScanner::NewPayables => ScanType::Payables, + HintableScanner::RetryPayables => ScanType::Payables, + HintableScanner::PendingPayables { .. } => ScanType::PendingPayables, + } + } +} + pub struct PayableScanScheduler { pub new_payable_notify_later: Box>, pub dyn_interval_computer: Box, @@ -250,8 +258,7 @@ impl ScheduleHintOnErrorResolverReal { ScanScheduleHint::DoNotSchedule } else if matches!(err, StartScanError::ScanAlreadyRunning { .. }) { unreachable!( - "an automatic scan of NewPayableScanner should never interfere with \ - itself {:?}", + "an automatic scan of NewPayableScanner should never interfere with itself {:?}", err ) } else { @@ -804,4 +811,28 @@ mod tests { ) }) } + + #[test] + fn conversion_between_hintable_scanner_and_scan_type_works() { + assert_eq!( + ScanType::from(HintableScanner::NewPayables), + ScanType::Payables + ); + assert_eq!( + ScanType::from(HintableScanner::RetryPayables), + ScanType::Payables + ); + assert_eq!( + ScanType::from(HintableScanner::PendingPayables { + initial_pending_payable_scan: false + }), + ScanType::PendingPayables + ); + assert_eq!( + ScanType::from(HintableScanner::PendingPayables { + initial_pending_payable_scan: true + }), + ScanType::PendingPayables + ); + } } diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 1b107a2eb..3a6384384 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -929,10 +929,10 @@ pub mod unshared_test_utils { if let Some(remaining) = self.stop_system_on_count_received_opt.borrow_mut().as_mut() { + *remaining -= 1; if remaining == &0 { System::current().stop(); } - *remaining -= 1; } if self.send_message_out { let handle = ctx.notify_later(msg, interval); From 3ac075a90d9130510c0372fefd7af938d3aed243 Mon Sep 17 00:00:00 2001 From: Bert Date: Thu, 22 May 2025 00:32:19 +0200 Subject: [PATCH 26/49] GH-602: fixed another couple of failing tests in accountant/mod.rs and scanners/mod.rs --- node/src/accountant/mod.rs | 133 ++++++++++------- node/src/accountant/scanners/mod.rs | 13 +- .../accountant/scanners/scan_schedulers.rs | 140 +++++++++--------- .../unprivileged_parse_args_configuration.rs | 3 +- node/src/test_utils/mod.rs | 19 ++- 5 files changed, 185 insertions(+), 123 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index bba4a1eef..7e38a1138 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -77,7 +77,7 @@ use std::rc::Rc; use std::str::RMatches; use std::time::SystemTime; use web3::types::H256; -use crate::accountant::scanners::scan_schedulers::{HintableScanner, ScanScheduleHint, ScanSchedulers}; +use crate::accountant::scanners::scan_schedulers::{HintableScanner, SchedulingAfterHaltedScan, ScanSchedulers}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; @@ -226,10 +226,12 @@ impl Handler for Accountant { // the RetryPayableScanner, which finishes, and the PendingPayablesScanner is scheduled // to run again. Therefore, not from here. let response_skeleton_opt = msg.response_skeleton_opt; - if let ScanScheduleHint::Schedule(ScanType::Payables) = + if let SchedulingAfterHaltedScan::Schedule(ScanType::Payables) = self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) { - self.scan_schedulers.payable.schedule_for_new_payable(ctx); + self.scan_schedulers + .payable + .schedule_for_new_payable(ctx, &self.logger); } } } @@ -245,11 +247,12 @@ impl Handler for Accountant { // be determined by the PendingPayableScanner, evaluating if it has seen all pending // payables complete. That opens up an opportunity for another run of the NewPayableScanner. let response_skeleton = msg.response_skeleton_opt; - if let ScanScheduleHint::Schedule(ScanType::Payables) = + if let SchedulingAfterHaltedScan::Schedule(ScanType::Payables) = self.handle_request_of_scan_for_new_payable(response_skeleton) { - todo!("make sure we've been here before, and if not, schedule"); - self.scan_schedulers.payable.schedule_for_new_payable(ctx) + self.scan_schedulers + .payable + .schedule_for_new_payable(ctx, &self.logger) } } } @@ -272,7 +275,7 @@ impl Handler for Accountant { // By this time we know it is an automatic scanner, which is always rescheduled right away, // no matter what its outcome is. self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); - self.scan_schedulers.receivable.schedule(ctx); + self.scan_schedulers.receivable.schedule(ctx, &self.logger); } } @@ -293,14 +296,15 @@ impl Handler for Accountant { // Externally triggered scan is not allowed to be a spark for a procedure that // would involve payables with fresh nonces. The job is done. } else { - todo!("Finishing PendingPayable scan. Automatic scanning."); - self.scan_schedulers.payable.schedule_for_new_payable(ctx) + self.scan_schedulers + .payable + .schedule_for_new_payable(ctx, &self.logger) } } PendingPayableScanResult::PaymentRetryRequired => self .scan_schedulers .payable - .schedule_for_retry_payable(ctx, response_skeleton_opt), + .schedule_for_retry_payable(ctx, response_skeleton_opt, &self.logger), }; } } @@ -329,7 +333,6 @@ impl Handler for Accountant { OperationOutcome::Failure => todo!(), }, Some(node_to_ui_msg) => { - todo!("Externally triggered payable scan is finishing"); self.ui_message_sub_opt .as_ref() .expect("UIGateway is not bound") @@ -337,9 +340,9 @@ impl Handler for Accountant { .expect("UIGateway is dead"); // When automatic scans are suppressed, the external triggers are not allowed to - // provoke a scan sequence spread across intervals. The only exception is + // provoke an unwinding scan sequence across intervals. The only exception is // the PendingPayableScanner and RetryPayableScanner, which are meant to run in - // tandem. + // a tight tandem. } } } @@ -898,7 +901,7 @@ impl Accountant { fn handle_request_of_scan_for_new_payable( &mut self, response_skeleton_opt: Option, - ) -> ScanScheduleHint { + ) -> SchedulingAfterHaltedScan { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( @@ -918,8 +921,7 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - todo!() - // ScanScheduleHint::DoNotSchedule + SchedulingAfterHaltedScan::DoNotSchedule } Err(e) => self.handle_start_scan_error_for_scanner_with_irregular_scheduling( HintableScanner::NewPayables, @@ -932,7 +934,7 @@ impl Accountant { fn handle_request_of_scan_for_retry_payable( &mut self, response_skeleton_opt: Option, - ) -> ScanScheduleHint { + ) -> SchedulingAfterHaltedScan { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_retry_payable_scan_guarded( @@ -951,7 +953,7 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - ScanScheduleHint::DoNotSchedule + SchedulingAfterHaltedScan::DoNotSchedule } Err(e) => { self.handle_start_scan_error_for_scanner_with_irregular_scheduling( @@ -979,8 +981,8 @@ impl Accountant { // // }); // // self.scan_schedulers - // .schedule_hint_on_error_resolver - // .resolve_hint_for_given_error(todo!(), &e, is_externally_triggered) + // .reschedule_on_error_resolver + // .resolve_rescheduling_for_given_error(todo!(), &e, is_externally_triggered) } } } @@ -988,7 +990,7 @@ impl Accountant { fn handle_request_of_scan_for_pending_payable( &mut self, response_skeleton_opt: Option, - ) -> ScanScheduleHint { + ) -> SchedulingAfterHaltedScan { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_pending_payable_scan_guarded( @@ -1001,14 +1003,14 @@ impl Accountant { None => Err(StartScanError::NoConsumingWalletFound), }; - let hint: ScanScheduleHint = match result { + let hint: SchedulingAfterHaltedScan = match result { Ok(scan_message) => { self.request_transaction_receipts_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - todo!("give a hint") + SchedulingAfterHaltedScan::DoNotSchedule } Err(e) => { let initial_pending_payable_scan = self.scanners.initial_pending_payable_scan(); @@ -1034,7 +1036,7 @@ impl Accountant { hintable_scanner: HintableScanner, e: StartScanError, response_skeleton_opt: Option, - ) -> ScanScheduleHint { + ) -> SchedulingAfterHaltedScan { let is_externally_triggered = response_skeleton_opt.is_some(); e.log_error( @@ -1055,8 +1057,8 @@ impl Accountant { }); self.scan_schedulers - .schedule_hint_on_error_resolver - .resolve_hint_for_given_error(hintable_scanner, &e, is_externally_triggered) + .reschedule_on_error_resolver + .resolve_rescheduling_for_given_error(hintable_scanner, &e, is_externally_triggered) } fn handle_request_of_scan_for_receivable( @@ -1519,7 +1521,13 @@ mod tests { let blockchain_bridge = blockchain_bridge .system_stop_conditions(match_lazily_every_type_id!(QualifiedPayablesMessage)); let blockchain_bridge_addr = blockchain_bridge.start(); + // Important + subject.scan_schedulers.automatic_scans_enabled = false; subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); + // Making sure we would get a panic if another scan was scheduled + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.nominal_interval = Duration::from_secs(100); let subject_addr = subject.start(); let system = System::new("test"); let ui_message = NodeFromUiMessage { @@ -1556,11 +1564,14 @@ mod tests { no_rowid_results: vec![], }); let payable_dao = PayableDaoMock::default().mark_pending_payables_rowids_result(Ok(())); - let subject = AccountantBuilder::default() + let mut subject = AccountantBuilder::default() .pending_payable_daos(vec![ForPayableScanner(pending_payable_dao)]) .payable_daos(vec![ForPayableScanner(payable_dao)]) .bootstrapper_config(config) .build(); + // Making sure we would get a panic if another scan was scheduled + subject.scan_schedulers.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); let subject_addr = subject.start(); let system = System::new("test"); @@ -1822,18 +1833,24 @@ mod tests { }; let pending_payable_dao = PendingPayableDaoMock::default() .return_all_errorless_fingerprints_result(vec![fingerprint.clone()]); - let subject = AccountantBuilder::default() + let mut subject = AccountantBuilder::default() .consuming_wallet(make_paying_wallet(b"consuming")) .bootstrapper_config(config) .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); - let subject_addr = subject.start(); + let blockchain_bridge = blockchain_bridge + .system_stop_conditions(match_lazily_every_type_id!(RequestTransactionReceipts)); + let blockchain_bridge_addr = blockchain_bridge.start(); let system = System::new("test"); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); + // Important + subject.scan_schedulers.automatic_scans_enabled = false; + subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); + // Making sure we would get a panic if another scan was scheduled + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.nominal_interval = Duration::from_secs(100); + let subject_addr = subject.start(); let ui_message = NodeFromUiMessage { client_id: 1234, body: UiScanRequest { @@ -1844,7 +1861,6 @@ mod tests { subject_addr.try_send(ui_message).unwrap(); - System::current().stop(); system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); assert_eq!( @@ -1866,8 +1882,16 @@ mod tests { "externally_triggered_scan_is_not_handled_in_case_the_scan_is_already_running"; let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); config.automatic_scans_enabled = false; + let now_unix = to_time_t(SystemTime::now()); + let payment_thresholds = PaymentThresholds::default(); + let past_timestamp_unix = now_unix + - (payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec) as i64; + let mut payable_account = make_payable_account(123); + payable_account.balance_wei = gwei_to_wei(payment_thresholds.debt_threshold_gwei); + payable_account.last_paid_timestamp = from_time_t(past_timestamp_unix); let payable_dao = - PayableDaoMock::default().non_pending_payables_result(vec![make_payable_account(123)]); + PayableDaoMock::default().non_pending_payables_result(vec![payable_account]); let subject = AccountantBuilder::default() .bootstrapper_config(config) .consuming_wallet(make_paying_wallet(b"consuming")) @@ -2246,6 +2270,8 @@ mod tests { fn accountant_requests_blockchain_bridge_to_scan_for_received_payments() { init_test_logging(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge = blockchain_bridge + .system_stop_conditions(match_lazily_every_type_id!(RetrieveTransactions)); let earning_wallet = make_wallet("someearningwallet"); let system = System::new("accountant_requests_blockchain_bridge_to_scan_for_received_payments"); @@ -2256,12 +2282,12 @@ mod tests { .bootstrapper_config(bc_from_earning_wallet(earning_wallet.clone())) .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) .build(); + // Important. Preventing the possibly endless sequence of + // PendingPayableScanner -> NewPayableScanner -> NewPayableScanner... + subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default()); subject .scanners .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); - subject - .scanners - .replace_scanner(ScannerReplacement::Payable(ReplacementType::Null)); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); let peer_actors = peer_actors_builder() @@ -2271,10 +2297,8 @@ mod tests { send_start_message!(accountant_subs); - System::current().stop(); system.run(); let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); - assert_eq!(blockchain_bridge_recorder.len(), 1); let retrieve_transactions_msg = blockchain_bridge_recorder.get_record::(0); assert_eq!( @@ -2284,6 +2308,7 @@ mod tests { response_skeleton_opt: None, } ); + assert_eq!(blockchain_bridge_recorder.len(), 1); } #[test] @@ -2297,17 +2322,19 @@ mod tests { let receivable_dao = ReceivableDaoMock::new() .new_delinquencies_result(vec![]) .paid_delinquencies_result(vec![]); - let subject = AccountantBuilder::default() + let mut subject = AccountantBuilder::default() .bootstrapper_config(config) .receivable_daos(vec![ForReceivableScanner(receivable_dao)]) .build(); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge = blockchain_bridge + .system_stop_conditions(match_lazily_every_type_id!(RetrieveTransactions)); + let blockchain_bridge_addr = blockchain_bridge.start(); + // Important + subject.scan_schedulers.automatic_scans_enabled = false; + subject.retrieve_transactions_sub_opt = Some(blockchain_bridge_addr.recipient()); let subject_addr = subject.start(); let system = System::new("test"); - let peer_actors = peer_actors_builder() - .blockchain_bridge(blockchain_bridge) - .build(); - subject_addr.try_send(BindMessage { peer_actors }).unwrap(); let ui_message = NodeFromUiMessage { client_id: 1234, body: UiScanRequest { @@ -2318,7 +2345,6 @@ mod tests { subject_addr.try_send(ui_message).unwrap(); - System::current().stop(); system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); assert_eq!( @@ -2648,7 +2674,7 @@ mod tests { let hint = subject.handle_request_of_scan_for_pending_payable(None); let flag_after = subject.scanners.initial_pending_payable_scan(); - assert_eq!(hint, ScanScheduleHint::DoNotSchedule); + assert_eq!(hint, SchedulingAfterHaltedScan::DoNotSchedule); assert_eq!(flag_before, true); assert_eq!(flag_after, false); } @@ -2665,7 +2691,10 @@ mod tests { let hint = subject.handle_request_of_scan_for_pending_payable(None); let flag_after = subject.scanners.initial_pending_payable_scan(); - assert_eq!(hint, ScanScheduleHint::Schedule(ScanType::Payables)); + assert_eq!( + hint, + SchedulingAfterHaltedScan::Schedule(ScanType::Payables) + ); assert_eq!(flag_before, true); assert_eq!(flag_after, false); } @@ -2721,12 +2750,12 @@ mod tests { .bootstrapper_config(config) .logger(Logger::new(test_name)) .build(); + // Important. Preventing the possibly endless sequence of + // PendingPayableScanner -> NewPayableScanner -> NewPayableScanner... + subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default()); subject .scanners - .replace_scanner(ScannerReplacement::Payable(ReplacementType::Null)); // Skipping - subject - .scanners - .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); // Skipping + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); subject .scanners .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Mock( diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 02b8d2981..49e0844c3 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -343,7 +343,7 @@ impl Scanners { payables to process." ) } else { - todo!() + Ok(()) } } } @@ -1736,7 +1736,7 @@ mod tests { self.receivable = Box::new(scanner) } ScannerReplacement::Receivable(ReplacementType::Null) => { - self.pending_payable = Box::new(NullScanner::default()) + self.receivable = Box::new(NullScanner::default()) } } } @@ -3069,6 +3069,8 @@ mod tests { let pending_payable_scanner = PendingPayableScannerBuilder::new() .pending_payable_dao(pending_payable_dao) .build(); + // Important + subject.aware_of_unresolved_pending_payable = true; subject.pending_payable = Box::new(pending_payable_scanner); let payable_scanner = PayableScannerBuilder::new().build(); subject.payable = Box::new(payable_scanner); @@ -3109,6 +3111,8 @@ mod tests { let pending_payable_scanner = PendingPayableScannerBuilder::new() .pending_payable_dao(pending_payable_dao) .build(); + // Important + subject.aware_of_unresolved_pending_payable = true; subject.pending_payable = Box::new(pending_payable_scanner); let payable_scanner = PayableScannerBuilder::new().build(); subject.payable = Box::new(payable_scanner); @@ -3141,6 +3145,8 @@ mod tests { let mut subject = make_dull_subject(); let pending_payable_scanner = PendingPayableScannerBuilder::new().build(); let payable_scanner = PayableScannerBuilder::new().build(); + // Important + subject.aware_of_unresolved_pending_payable = true; subject.pending_payable = Box::new(pending_payable_scanner); subject.payable = Box::new(payable_scanner); let logger = Logger::new("test"); @@ -3178,6 +3184,7 @@ mod tests { .checked_sub(Duration::from_millis(12)) .unwrap(); let timestamp_payable_scanner_start = SystemTime::now(); + subject.aware_of_unresolved_pending_payable = true; subject .pending_payable .mark_as_started(timestamp_pending_payable_start); @@ -3237,7 +3244,7 @@ mod tests { fn pending_payable_scanner_bumps_into_zero_pending_payable_awareness_in_the_automatic_mode() { let consuming_wallet = make_paying_wallet(b"consuming"); let mut subject = make_dull_subject(); - let mut pending_payable_scanner = PendingPayableScannerBuilder::new().build(); + let pending_payable_scanner = PendingPayableScannerBuilder::new().build(); subject.pending_payable = Box::new(pending_payable_scanner); subject.aware_of_unresolved_pending_payable = false; diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index b7a990b28..27f633274 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -10,8 +10,10 @@ use crate::sub_lib::utils::{ NotifyHandle, NotifyHandleReal, NotifyLaterHandle, NotifyLaterHandleReal, }; use actix::{Actor, Context, Handler}; +use masq_lib::logger::Logger; use masq_lib::messages::ScanType; use std::cell::RefCell; +use std::fmt::Debug; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -19,7 +21,7 @@ pub struct ScanSchedulers { pub payable: PayableScanScheduler, pub pending_payable: SimplePeriodicalScanScheduler, pub receivable: SimplePeriodicalScanScheduler, - pub schedule_hint_on_error_resolver: Box, + pub reschedule_on_error_resolver: Box, pub automatic_scans_enabled: bool, } @@ -31,7 +33,7 @@ impl ScanSchedulers { scan_intervals.pending_payable_scan_interval, ), receivable: SimplePeriodicalScanScheduler::new(scan_intervals.receivable_scan_interval), - schedule_hint_on_error_resolver: Box::new(ScheduleHintOnErrorResolverReal::default()), + reschedule_on_error_resolver: Box::new(RescheduleScanOnErrorResolverReal::default()), automatic_scans_enabled, } } @@ -43,7 +45,7 @@ pub enum PayableScanSchedulerError { } #[derive(Debug, PartialEq)] -pub enum ScanScheduleHint { +pub enum SchedulingAfterHaltedScan { Schedule(ScanType), DoNotSchedule, } @@ -86,7 +88,7 @@ impl PayableScanScheduler { } } - pub fn schedule_for_new_payable(&self, ctx: &mut Context) { + pub fn schedule_for_new_payable(&self, ctx: &mut Context, logger: &Logger) { let inner = self.inner.lock().expect("couldn't acquire inner"); let last_new_payable_timestamp = inner.last_new_payable_scan_timestamp; let nominal_interval = self.nominal_interval; @@ -96,6 +98,12 @@ impl PayableScanScheduler { last_new_payable_timestamp, nominal_interval, ) { + debug!( + logger, + "Scheduling a new-payable scan in {}ms", + interval.as_millis() + ); + let _ = self.new_payable_notify_later.notify_later( ScanForNewPayables { response_skeleton_opt: None, @@ -104,6 +112,8 @@ impl PayableScanScheduler { ctx, ); } else { + debug!(logger, "Scheduling a new-payable scan asap"); + let _ = self.new_payable_notify.notify( ScanForNewPayables { response_skeleton_opt: None, @@ -120,7 +130,10 @@ impl PayableScanScheduler { &self, ctx: &mut Context, response_skeleton_opt: Option, + logger: &Logger, ) { + debug!(logger, "Scheduling a retry-payable scan asap"); + self.retry_payable_notify.notify( ScanForRetryPayables { response_skeleton_opt, @@ -180,29 +193,34 @@ impl NewPayableScanDynIntervalComputer for NewPayableScanDynIntervalComputerReal } pub struct SimplePeriodicalScanScheduler { - pub is_currently_automatically_scheduled: RefCell, pub handle: Box>, pub interval: Duration, } impl SimplePeriodicalScanScheduler where - Message: actix::Message + Default + 'static + Send, + Message: actix::Message + Default + Debug + Send + 'static, Accountant: Actor + Handler, { fn new(interval: Duration) -> Self { Self { - is_currently_automatically_scheduled: RefCell::new(false), handle: Box::new(NotifyLaterHandleReal::default()), interval, } } - pub fn schedule(&self, ctx: &mut Context) { + pub fn schedule(&self, ctx: &mut Context, logger: &Logger) { // The default of the message implies response_skeleton_opt to be None because scheduled // scans don't respond - let _ = self - .handle - .notify_later(Message::default(), self.interval, ctx); + let msg = Message::default(); + + debug!( + logger, + "Scheduling a scan with {:?} in {}ms", + msg, + self.interval.as_millis() + ); + + let _ = self.handle.notify_later(msg, self.interval, ctx); } } @@ -212,25 +230,25 @@ where // repetition of the erroneous scan) to prevent a full panic, while ensuring no unresolved issues // are left for future scans. A panic is justified only if the error is deemed impossible by design // within the broader context of that location. -pub trait ScheduleHintOnErrorResolver { - fn resolve_hint_for_given_error( +pub trait RescheduleScanOnErrorResolver { + fn resolve_rescheduling_for_given_error( &self, hintable_scanner: HintableScanner, error: &StartScanError, is_externally_triggered: bool, - ) -> ScanScheduleHint; + ) -> SchedulingAfterHaltedScan; } #[derive(Default)] -pub struct ScheduleHintOnErrorResolverReal {} +pub struct RescheduleScanOnErrorResolverReal {} -impl ScheduleHintOnErrorResolver for ScheduleHintOnErrorResolverReal { - fn resolve_hint_for_given_error( +impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverReal { + fn resolve_rescheduling_for_given_error( &self, hintable_scanner: HintableScanner, error: &StartScanError, is_externally_triggered: bool, - ) -> ScanScheduleHint { + ) -> SchedulingAfterHaltedScan { match hintable_scanner { HintableScanner::NewPayables => { Self::resolve_new_payables(error, is_externally_triggered) @@ -249,20 +267,20 @@ impl ScheduleHintOnErrorResolver for ScheduleHintOnErrorResolverReal { } } -impl ScheduleHintOnErrorResolverReal { +impl RescheduleScanOnErrorResolverReal { fn resolve_new_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> ScanScheduleHint { + ) -> SchedulingAfterHaltedScan { if is_externally_triggered { - ScanScheduleHint::DoNotSchedule + SchedulingAfterHaltedScan::DoNotSchedule } else if matches!(err, StartScanError::ScanAlreadyRunning { .. }) { unreachable!( "an automatic scan of NewPayableScanner should never interfere with itself {:?}", err ) } else { - ScanScheduleHint::Schedule(ScanType::Payables) + SchedulingAfterHaltedScan::Schedule(ScanType::Payables) } } @@ -272,9 +290,9 @@ impl ScheduleHintOnErrorResolverReal { fn resolve_retry_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> ScanScheduleHint { + ) -> SchedulingAfterHaltedScan { if is_externally_triggered { - ScanScheduleHint::DoNotSchedule + SchedulingAfterHaltedScan::DoNotSchedule } else { unreachable!( "{:?} is not acceptable for an automatic scan of RetryPayablesScanner", @@ -287,12 +305,12 @@ impl ScheduleHintOnErrorResolverReal { err: &StartScanError, initial_pending_payable_scan: bool, is_externally_triggered: bool, - ) -> ScanScheduleHint { + ) -> SchedulingAfterHaltedScan { if is_externally_triggered { - ScanScheduleHint::DoNotSchedule + SchedulingAfterHaltedScan::DoNotSchedule } else if err == &StartScanError::NothingToProcess { if initial_pending_payable_scan { - ScanScheduleHint::Schedule(ScanType::Payables) + SchedulingAfterHaltedScan::Schedule(ScanType::Payables) } else { unreachable!( "the automatic pending payable scan should always be requested only in need, \ @@ -300,7 +318,7 @@ impl ScheduleHintOnErrorResolverReal { ) } } else if err == &StartScanError::NoConsumingWalletFound { - ScanScheduleHint::Schedule(ScanType::Payables) + SchedulingAfterHaltedScan::Schedule(ScanType::Payables) } else { unreachable!( "{:?} is not acceptable for an automatic scan of PendingPayableScanner", @@ -314,7 +332,7 @@ impl ScheduleHintOnErrorResolverReal { mod tests { use crate::accountant::scanners::scan_schedulers::{ HintableScanner, NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, - ScanScheduleHint, ScanSchedulers, + ScanSchedulers, SchedulingAfterHaltedScan, }; use crate::accountant::scanners::{MTError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; @@ -352,24 +370,10 @@ mod tests { schedulers.pending_payable.interval, scan_intervals.pending_payable_scan_interval ); - assert_eq!( - *schedulers - .pending_payable - .is_currently_automatically_scheduled - .borrow(), - false - ); assert_eq!( schedulers.receivable.interval, scan_intervals.receivable_scan_interval ); - assert_eq!( - *schedulers - .receivable - .is_currently_automatically_scheduled - .borrow(), - false - ); assert_eq!(schedulers.automatic_scans_enabled, true) } @@ -558,7 +562,7 @@ mod tests { } #[test] - fn resolve_hint_for_given_error_works_for_pending_payables_if_externally_triggered() { + fn resolve_rescheduling_for_given_error_works_for_pending_payables_if_externally_triggered() { let subject = ScanSchedulers::new(ScanIntervals::default(), true); test_what_if_externally_triggered( @@ -584,12 +588,12 @@ mod tests { .enumerate() .for_each(|(idx, (error))| { let result = subject - .schedule_hint_on_error_resolver - .resolve_hint_for_given_error(hintable_scanner, error, true); + .reschedule_on_error_resolver + .resolve_rescheduling_for_given_error(hintable_scanner, error, true); assert_eq!( result, - ScanScheduleHint::DoNotSchedule, + SchedulingAfterHaltedScan::DoNotSchedule, "We expected DoNotSchedule but got {:?} at idx {} for {:?}", result, idx, @@ -604,8 +608,8 @@ mod tests { let subject = ScanSchedulers::new(ScanIntervals::default(), true); let result = subject - .schedule_hint_on_error_resolver - .resolve_hint_for_given_error( + .reschedule_on_error_resolver + .resolve_rescheduling_for_given_error( HintableScanner::PendingPayables { initial_pending_payable_scan: true, }, @@ -615,7 +619,7 @@ mod tests { assert_eq!( result, - ScanScheduleHint::Schedule(ScanType::Payables), + SchedulingAfterHaltedScan::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got {:?}", result, ); @@ -632,8 +636,8 @@ mod tests { let subject = ScanSchedulers::new(ScanIntervals::default(), true); let _ = subject - .schedule_hint_on_error_resolver - .resolve_hint_for_given_error( + .reschedule_on_error_resolver + .resolve_rescheduling_for_given_error( HintableScanner::PendingPayables { initial_pending_payable_scan: false, }, @@ -649,8 +653,8 @@ mod tests { hintable_scanner: HintableScanner, ) { let result = subject - .schedule_hint_on_error_resolver - .resolve_hint_for_given_error( + .reschedule_on_error_resolver + .resolve_rescheduling_for_given_error( hintable_scanner, &StartScanError::NoConsumingWalletFound, false, @@ -658,7 +662,7 @@ mod tests { assert_eq!( result, - ScanScheduleHint::Schedule(ScanType::Payables), + SchedulingAfterHaltedScan::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got {:?} for {:?}", result, hintable_scanner @@ -691,8 +695,8 @@ mod tests { inputs.errors.iter().for_each(|error| { let panic = catch_unwind(AssertUnwindSafe(|| { subject - .schedule_hint_on_error_resolver - .resolve_hint_for_given_error( + .reschedule_on_error_resolver + .resolve_rescheduling_for_given_error( HintableScanner::PendingPayables { initial_pending_payable_scan, }, @@ -728,7 +732,7 @@ mod tests { } #[test] - fn resolve_hint_for_given_error_works_for_retry_payables_if_externally_triggered() { + fn resolve_rescheduling_for_given_error_works_for_retry_payables_if_externally_triggered() { let subject = ScanSchedulers::new(ScanIntervals::default(), true); test_what_if_externally_triggered(&subject, HintableScanner::RetryPayables {}); @@ -741,8 +745,12 @@ mod tests { ALL_START_SCAN_ERRORS.iter().for_each(|error| { let panic = catch_unwind(AssertUnwindSafe(|| { subject - .schedule_hint_on_error_resolver - .resolve_hint_for_given_error(HintableScanner::RetryPayables, error, false) + .reschedule_on_error_resolver + .resolve_rescheduling_for_given_error( + HintableScanner::RetryPayables, + error, + false, + ) })) .unwrap_err(); @@ -761,7 +769,7 @@ mod tests { } #[test] - fn resolve_hint_for_given_error_works_for_new_payables_if_externally_triggered() { + fn resolve_rescheduling_for_given_error_works_for_new_payables_if_externally_triggered() { let subject = ScanSchedulers::new(ScanIntervals::default(), true); test_what_if_externally_triggered(&subject, HintableScanner::NewPayables {}); @@ -776,8 +784,8 @@ mod tests { let subject = ScanSchedulers::new(ScanIntervals::default(), true); let _ = subject - .schedule_hint_on_error_resolver - .resolve_hint_for_given_error( + .reschedule_on_error_resolver + .resolve_rescheduling_for_given_error( HintableScanner::NewPayables, &StartScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Payables, @@ -800,12 +808,12 @@ mod tests { inputs.errors.iter().for_each(|error| { let result = subject - .schedule_hint_on_error_resolver - .resolve_hint_for_given_error(HintableScanner::NewPayables, *error, false); + .reschedule_on_error_resolver + .resolve_rescheduling_for_given_error(HintableScanner::NewPayables, *error, false); assert_eq!( result, - ScanScheduleHint::Schedule(ScanType::Payables), + SchedulingAfterHaltedScan::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got '{:?}'", result, ) diff --git a/node/src/node_configurator/unprivileged_parse_args_configuration.rs b/node/src/node_configurator/unprivileged_parse_args_configuration.rs index 66c3c5247..801aa4456 100644 --- a/node/src/node_configurator/unprivileged_parse_args_configuration.rs +++ b/node/src/node_configurator/unprivileged_parse_args_configuration.rs @@ -504,8 +504,9 @@ fn configure_accountant_config( |pc: &dyn PersistentConfiguration| pc.scan_intervals(), |pc: &mut dyn PersistentConfiguration, intervals| pc.set_scan_intervals(intervals), )?; + let automatic_scans_enabled = - value_m!(multi_config, "scans", String).unwrap_or_else(|| "on".to_string()) == *"off"; + value_m!(multi_config, "scans", String).unwrap_or_else(|| "on".to_string()) == "on"; config.payment_thresholds_opt = Some(payment_thresholds); config.scan_intervals_opt = Some(scan_intervals); diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 3a6384384..0c2b3de5a 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -872,11 +872,14 @@ pub mod unshared_test_utils { pub mod notify_handlers { use super::*; + use std::fmt::Debug; pub struct NotifyLaterHandleMock { notify_later_params: Arc>>, stop_system_on_count_received_opt: RefCell>, send_message_out: bool, + // To prove that no msg was tried to be scheduled + panic_on_schedule_attempt: bool, } impl Default for NotifyLaterHandleMock { @@ -885,6 +888,7 @@ pub mod unshared_test_utils { notify_later_params: Arc::new(Mutex::new(vec![])), stop_system_on_count_received_opt: RefCell::new(None), send_message_out: false, + panic_on_schedule_attempt: false, } } } @@ -909,11 +913,16 @@ pub mod unshared_test_utils { self.send_message_out = true; self } + + pub fn panic_on_schedule_attempt(mut self) -> Self { + self.panic_on_schedule_attempt = true; + self + } } impl NotifyLaterHandle for NotifyLaterHandleMock where - M: Message + 'static + Clone + Send, + M: Message + Clone + Debug + Send + 'static, A: Actor> + Handler, { fn notify_later<'a>( @@ -922,6 +931,14 @@ pub mod unshared_test_utils { interval: Duration, ctx: &'a mut Context, ) -> Box { + if self.panic_on_schedule_attempt { + panic!( + "Message scheduling request for {:?} and interval {}ms, thought not \ + expected", + msg, + interval.as_millis() + ); + } self.notify_later_params .lock() .unwrap() From a5f0945d7714b15f6e89581be4ef5a5063048353 Mon Sep 17 00:00:00 2001 From: Bert Date: Thu, 22 May 2025 01:21:13 +0200 Subject: [PATCH 27/49] GH-602: adding another little pile --- node/src/accountant/mod.rs | 18 +++++++++--------- node/src/accountant/scanners/mod.rs | 7 +++---- .../src/accountant/scanners/scan_schedulers.rs | 12 ++++++------ 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 7e38a1138..6a027ba52 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1937,8 +1937,8 @@ mod tests { scan_type: ScanType, ) { let expected_log_msg = format!( - "WARN: {test_name}: Manual {:?} scan was denied. Automatic scanning \ - setup prevents manual triggers.", + "WARN: {test_name}: Manual {:?} scan was denied. Automatic mode \ + prevents manual triggers.", scan_type ); @@ -2701,22 +2701,22 @@ mod tests { #[test] #[should_panic( - expected = "internal error: entered unreachable code: Initial scan for pending payable unexpected error: bluh" + expected = "internal error: entered unreachable code: ScanAlreadyRunning { \ + pertinent_scanner: PendingPayables, started_at: SystemTime { tv_sec: 0, tv_nsec: 0 } } \ + should be impossible with PendingPayableScanner in automatic mode" )] fn initial_pending_payable_scan_hits_unexpected_error() { init_test_logging(); - let mut subject = AccountantBuilder::default().build(); + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("abc")) + .build(); let pending_payable_scanner = - ScannerMock::default().start_scan_result(Err(StartScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::Payables, - started_at: SystemTime::now(), - })); + ScannerMock::default().scan_started_at_result(Some(UNIX_EPOCH)); subject .scanners .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( pending_payable_scanner, ))); - let flag_before = subject.scanners.initial_pending_payable_scan(); let _ = subject.handle_request_of_scan_for_pending_payable(None); } diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 49e0844c3..f2a845db1 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -283,7 +283,7 @@ impl Scanners { msg: BlockchainAgentWithContextMessage, logger: &Logger, ) -> Result, String> { - todo!() + self.payable.try_skipping_payment_adjustment(msg, logger) } pub fn perform_payable_adjustment( @@ -291,7 +291,7 @@ impl Scanners { setup: PreparedAdjustment, logger: &Logger, ) -> OutboundPaymentsInstructions { - todo!() + self.payable.perform_payment_adjustment(setup, logger) } pub fn initial_pending_payable_scan(&self) -> bool { @@ -1302,8 +1302,7 @@ impl StartScanError { }, StartScanError::ManualTriggerError(e) => match e { MTError::AutomaticScanConflict => ErrorType::Permanent(format!( - "Manual {:?} scan was denied. Automatic scanning setup prevents manual \ - triggers.", + "Manual {:?} scan was denied. Automatic mode prevents manual triggers.", scan_type )), MTError::UnnecessaryRequest { hint_opt } => ErrorType::Temporary(format!( diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 27f633274..1dd5a7906 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -295,7 +295,7 @@ impl RescheduleScanOnErrorResolverReal { SchedulingAfterHaltedScan::DoNotSchedule } else { unreachable!( - "{:?} is not acceptable for an automatic scan of RetryPayablesScanner", + "{:?} should be impossible with RetryPayableScanner in automatic mode", err ) } @@ -321,7 +321,7 @@ impl RescheduleScanOnErrorResolverReal { SchedulingAfterHaltedScan::Schedule(ScanType::Payables) } else { unreachable!( - "{:?} is not acceptable for an automatic scan of PendingPayableScanner", + "{:?} should be impossible with PendingPayableScanner in automatic mode", err ) } @@ -708,8 +708,8 @@ mod tests { let panic_msg = panic.downcast_ref::().unwrap(); let expected_msg = format!( - "internal error: entered unreachable code: {:?} is not acceptable for \ - an automatic scan of PendingPayableScanner", + "internal error: entered unreachable code: {:?} should be impossible with \ + PendingPayableScanner in automatic mode", error ); assert_eq!( @@ -756,8 +756,8 @@ mod tests { let panic_msg = panic.downcast_ref::().unwrap(); let expected_msg = format!( - "internal error: entered unreachable code: {:?} is not acceptable for an automatic \ - scan of RetryPayablesScanner", + "internal error: entered unreachable code: {:?} should be impossible \ + with RetryPayablesScanner in automatic mode.", error ); assert_eq!( From af42a1269deb69dbc69777f4109c59aaaf42b884 Mon Sep 17 00:00:00 2001 From: Bert Date: Sat, 24 May 2025 21:23:10 +0200 Subject: [PATCH 28/49] GH-602: closing the gap... probably only one test to be written yet --- masq_lib/src/messages.rs | 1 - node/src/accountant/mod.rs | 238 +++++++++++++----- node/src/accountant/scanners/mod.rs | 115 +++++++-- .../scanners/payable_scanner_extension/mod.rs | 1 - .../accountant/scanners/scan_schedulers.rs | 15 +- .../src/accountant/scanners/scanners_utils.rs | 2 +- node/src/accountant/scanners/test_utils.rs | 1 - node/src/test_utils/mod.rs | 2 +- node/src/test_utils/recorder.rs | 1 - 9 files changed, 271 insertions(+), 105 deletions(-) diff --git a/masq_lib/src/messages.rs b/masq_lib/src/messages.rs index 9f2504e1b..19fa128b2 100644 --- a/masq_lib/src/messages.rs +++ b/masq_lib/src/messages.rs @@ -11,7 +11,6 @@ use serde_derive::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt; use std::fmt::Debug; -use std::future::pending; use std::str::FromStr; pub const NODE_UI_PROTOCOL: &str = "MASQNode-UIv2"; diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 6a027ba52..c65265058 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -74,7 +74,6 @@ use std::fmt::Display; use std::ops::{Div, Mul}; use std::path::Path; use std::rc::Rc; -use std::str::RMatches; use std::time::SystemTime; use web3::types::H256; use crate::accountant::scanners::scan_schedulers::{HintableScanner, SchedulingAfterHaltedScan, ScanSchedulers}; @@ -143,7 +142,7 @@ pub struct ReportTransactionReceipts { pub response_skeleton_opt: Option, } -#[derive(Debug, Message, PartialEq)] +#[derive(Debug, Message, PartialEq, Clone)] pub struct SentPayables { pub payment_procedure_result: Result, PayableTransactionError>, pub response_skeleton_opt: Option, @@ -329,8 +328,14 @@ impl Handler for Accountant { match scan_result.ui_response_opt { None => match scan_result.result { - OperationOutcome::NewPendingPayable => todo!(), - OperationOutcome::Failure => todo!(), + OperationOutcome::NewPendingPayable => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + OperationOutcome::Failure => self + .scan_schedulers + .payable + .schedule_for_new_payable(ctx, &self.logger), }, Some(node_to_ui_msg) => { self.ui_message_sub_opt @@ -367,18 +372,8 @@ impl Handler for Accountant { fn handle(&mut self, scan_error: ScanError, _ctx: &mut Self::Context) -> Self::Result { error!(self.logger, "Received ScanError: {:?}", scan_error); - self.scanners.scan_error_scan_reset(&scan_error); - // match scan_error.scan_type { - // ScanType::Payables => { - // self.scanners.payable.mark_as_ended(&self.logger); - // } - // ScanType::PendingPayables => { - // self.scanners.pending_payable.mark_as_ended(&self.logger); - // } - // ScanType::Receivables => { - // self.scanners.receivable.mark_as_ended(&self.logger); - // } - // }; + self.scanners + .acknowledge_scan_error(&scan_error, &self.logger); if let Some(response_skeleton) = scan_error.response_skeleton_opt { let error_msg = NodeToUiMessage { target: ClientId(response_skeleton.client_id), @@ -1231,11 +1226,11 @@ mod tests { use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; use crate::accountant::scanners::test_utils::{MarkScanner, NewPayableScanDynIntervalComputerMock, ReplacementType, ScannerReplacement}; - use crate::accountant::scanners::{StartScanError, ReceivableScanner, PendingPayableScanner}; + use crate::accountant::scanners::{StartScanError}; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; - use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock, DaoWithDestination}; + use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock}; use crate::accountant::test_utils::{AccountantBuilder, BannedDaoMock}; use crate::accountant::Accountant; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; @@ -1269,7 +1264,7 @@ mod tests { use actix::{System}; use ethereum_types::U64; use ethsign_crypto::Keccak256; - use log::{logger, Level}; + use log::{Level}; use masq_lib::constants::{ REQUEST_WITH_MUTUALLY_EXCLUSIVE_PARAMS, REQUEST_WITH_NO_VALUES, SCAN_ERROR, VALUE_EXCEEDS_ALLOWED_LIMIT, @@ -1290,13 +1285,10 @@ mod tests { use std::sync::Mutex; use std::time::{Duration, UNIX_EPOCH}; use std::vec; - use actix::fut::Either::B; - use nix::sys::socket::sockopt::BindToDevice; - use crate::accountant::scanners::local_test_utils::{NullScanner, ScannerMock}; + use crate::accountant::scanners::local_test_utils::{ScannerMock}; use crate::accountant::scanners::scan_schedulers::{NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; - use crate::sub_lib::tokio_wrappers::TokioListenerWrapperFactoryReal; use crate::test_utils::recorder_counter_msgs::SingleCounterMsgSetup; impl Handler> for Accountant { @@ -1685,16 +1677,22 @@ mod tests { agent_id_stamp ); assert_eq!(blockchain_bridge_recording.len(), 1); - test_use_of_the_same_logger(&logger_clone, test_name) + test_use_of_the_same_logger(&logger_clone, test_name, None) // adjust_payments() did not need a prepared result, which means it wasn't reached // because otherwise this test would've panicked } - fn test_use_of_the_same_logger(logger_clone: &Logger, test_name: &str) { - let experiment_msg = format!("DEBUG: {test_name}: hello world"); + fn test_use_of_the_same_logger( + logger_clone: &Logger, + test_name: &str, + differentiation_opt: Option<&str>, + ) { let log_handler = TestLogHandler::default(); + let experiment_msg = format!("DEBUG: {test_name}: hello world: {:?}", differentiation_opt); log_handler.exists_no_log_containing(&experiment_msg); - debug!(logger_clone, "hello world"); + + debug!(logger_clone, "hello world: {:?}", differentiation_opt); + log_handler.exists_log_containing(&experiment_msg); } @@ -1811,7 +1809,7 @@ mod tests { Some(response_skeleton) ); assert_eq!(blockchain_bridge_recording.len(), 1); - test_use_of_the_same_logger(&logger_clone, test_name) + test_use_of_the_same_logger(&logger_clone, test_name, None) } #[test] @@ -2036,56 +2034,82 @@ mod tests { #[test] fn pending_payable_scan_response_is_sent_to_ui_gateway_when_both_participating_scanners_have_completed( ) { - todo!("fix me"); - let mut config = bc_from_earning_wallet(make_wallet("earning_wallet")); - config.scan_intervals_opt = Some(ScanIntervals { - payable_scan_interval: Duration::from_millis(10_000), - receivable_scan_interval: Duration::from_millis(10_000), - pending_payable_scan_interval: Duration::from_secs(100), + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 4555, + context_id: 5566, }); + // TODO when we have more logic in place with the other cards taken in, we'll need to configure these + // accordingly let payable_dao = PayableDaoMock::default().transactions_confirmed_result(Ok(())); - let pending_payable = PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); - let subject = AccountantBuilder::default() - .bootstrapper_config(config) + let pending_payable = PendingPayableDaoMock::default() + .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]) + .mark_failures_result(Ok(())); + let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("consuming")) .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable)]) .build(); + subject.scan_schedulers.automatic_scans_enabled = false; + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let (peer_actors, peer_addresses) = peer_actors_builder() + .blockchain_bridge(blockchain_bridge) + .ui_gateway(ui_gateway) + .build_and_provide_addresses(); let subject_addr = subject.start(); let system = System::new("test"); - let peer_actors = peer_actors_builder().ui_gateway(ui_gateway).build(); + peer_addresses + .blockchain_bridge_addr + .try_send(SetUpCounterMsgs::new(vec![ + setup_for_counter_msg_triggered_via_type_id!( + RequestTransactionReceipts, + ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash: make_tx_hash(234), + status: TxStatus::Failed + }), + make_pending_payable_fingerprint() + )], + response_skeleton_opt + }, + &subject_addr + ), + setup_for_counter_msg_triggered_via_type_id!( + QualifiedPayablesMessage, + SentPayables { + payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( + PendingPayable { + recipient_wallet: make_wallet("abc"), + hash: make_tx_hash(789) + } + )]), + response_skeleton_opt + }, + &subject_addr + ), + ])) + .unwrap(); subject_addr.try_send(BindMessage { peer_actors }).unwrap(); - let tx_receipt = TxReceipt { - transaction_hash: make_tx_hash(456), - status: TxStatus::Succeeded(TransactionBlock { - block_hash: make_tx_hash(789), - block_number: 789012345.into(), - }), - }; - let pending_payable_fingerprint = make_pending_payable_fingerprint(); - let report_transaction_receipts = ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(tx_receipt), - pending_payable_fingerprint, - )], - response_skeleton_opt: Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }), + let pending_payable_request = ScanForPendingPayables { + response_skeleton_opt, }; - subject_addr.try_send(report_transaction_receipts).unwrap(); + subject_addr.try_send(pending_payable_request).unwrap(); - System::current().stop(); system.run(); let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); assert_eq!( ui_gateway_recording.get_record::(0), &NodeToUiMessage { - target: ClientId(1234), - body: UiScanResponse {}.tmb(4321), + target: ClientId(4555), + body: UiScanResponse {}.tmb(5566), } ); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + assert_eq!(blockchain_bridge_recording.len(), 2); } #[test] @@ -2263,7 +2287,7 @@ mod tests { let message = blockchain_bridge_recorder.get_record::(0); assert_eq!(message, &qualified_payables_msg); assert_eq!(blockchain_bridge_recorder.len(), 1); - test_use_of_the_same_logger(&actual_logger, test_name) + test_use_of_the_same_logger(&actual_logger, test_name, None) } #[test] @@ -2479,6 +2503,7 @@ mod tests { // We do ensure the PendingPayableScanner runs before the NewPayableScanner. It does not // matter a lot when does the ReceivableScanner take place. init_test_logging(); + let test_name = "accountant_scans_after_startup"; let pending_payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); let receivable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); let payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); @@ -2506,6 +2531,7 @@ mod tests { .start_scan_params(&payable_start_scan_params_arc) .start_scan_result(Err(StartScanError::NothingToProcess)); let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) .bootstrapper_config(config) .build(); subject @@ -2593,6 +2619,7 @@ mod tests { "We did not expect to see another schedule for pending payables, but it happened {:?}", scan_for_pending_payables_notify_later_params ); + test_use_of_the_same_logger(&pp_logger, test_name, Some("pp")); // Assertions on the payable scanner proper functioning // First, there is no functionality from the payable scanner actually running. // We only witness it to be scheduled. @@ -2629,7 +2656,7 @@ mod tests { ); // Assertions on the receivable scanner proper functioning let mut receivable_start_scan_params = receivable_start_scan_params_arc.lock().unwrap(); - let (r_wallet, r_started_at, r_response_skeleton_opt, r_logger, r_trigger_msg_name_str) = + let (r_wallet, _r_started_at, r_response_skeleton_opt, r_logger, r_trigger_msg_name_str) = receivable_start_scan_params.remove(0); assert_eq!(r_wallet, earning_wallet); assert_eq!(r_response_skeleton_opt, None); @@ -2643,6 +2670,7 @@ mod tests { "Should be already empty but was {:?}", receivable_start_scan_params ); + test_use_of_the_same_logger(&r_logger, test_name, Some("r")); let scan_for_receivables_notify_later_params = scan_for_receivables_notify_later_params_arc.lock().unwrap(); assert_eq!( @@ -2666,6 +2694,7 @@ mod tests { let pending_payable_dao = PendingPayableDaoMock::default() .return_all_errorless_fingerprints_result(vec![make_pending_payable_fingerprint()]); let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("consuming")) .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .build(); subject.request_transaction_receipts_sub_opt = Some(make_recorder().0.start().recipient()); @@ -2684,6 +2713,7 @@ mod tests { let pending_payable_dao = PendingPayableDaoMock::default().return_all_errorless_fingerprints_result(vec![]); let mut subject = AccountantBuilder::default() + .consuming_wallet(make_wallet("consuming")) .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .build(); let flag_before = subject.scanners.initial_pending_payable_scan(); @@ -3198,7 +3228,7 @@ mod tests { }); let now = to_time_t(SystemTime::now()); let qualified_payables = vec![ - // slightly above minimum balance, to the right of the curve (time intersection) + // slightly above the minimum balance, to the right of the curve (time intersection) PayableAccount { wallet: make_wallet("wallet0"), balance_wei: gwei_to_wei( @@ -3242,7 +3272,9 @@ mod tests { subject .scanners .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); + subject.scan_schedulers.automatic_scans_enabled = false; subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); + bind_ui_gateway_unasserted(&mut subject); let response_skeleton_opt = Some(ResponseSkeleton { client_id: 31, context_id: 24, @@ -3301,7 +3333,10 @@ mod tests { }), }; let message_simultaneous = ScanForNewPayables { - response_skeleton_opt: None, + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 999, + context_id: 888, + }), }; let message_after = ScanForNewPayables { response_skeleton_opt: Some(ResponseSkeleton { @@ -3310,6 +3345,9 @@ mod tests { }), }; subject.qualified_payables_sub_opt = Some(qualified_payables_sub); + bind_ui_gateway_unasserted(&mut subject); + // important + subject.scan_schedulers.automatic_scans_enabled = false; let addr = subject.start(); addr.try_send(message_before.clone()).unwrap(); @@ -3320,7 +3358,10 @@ mod tests { // that the third scan request is going to be accepted willingly again. addr.try_send(SentPayables { payment_procedure_result: Err(PayableTransactionError::Signing("bluh".to_string())), - response_skeleton_opt: None, + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 1122, + context_id: 7788, + }), }) .unwrap(); addr.try_send(message_after.clone()).unwrap(); @@ -3341,7 +3382,7 @@ mod tests { let messages_received = blockchain_bridge_recording.len(); assert_eq!(messages_received, 2); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {}: Payables scan was already initiated", + "INFO: {}: Payables scan was already initiated", test_name )); } @@ -4096,6 +4137,67 @@ mod tests { // ScanForRetryPayables) } + #[test] + fn zero_sent_payables_so_the_payable_scan_is_rescheduled_without_scanning_for_pending_payables() + { + init_test_logging(); + let test_name = "zero_sent_payables_so_the_payable_scan_is_rescheduled_without_scanning_for_pending_payables"; + let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let system = System::new(test_name); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + ScannerMock::default() + .finish_scan_params(&finish_scan_params_arc) + .finish_scan_result(PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::Failure, + }), + ))); + // Important. Otherwise, the scan would've been handled through a different endpoint and + // gone for a very long time + subject + .scan_schedulers + .payable + .inner + .lock() + .unwrap() + .last_new_payable_scan_timestamp = SystemTime::now(); + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default().notify_later_params(&payable_notify_later_params_arc), + ); + let sent_payable = SentPayables { + payment_procedure_result: Err(PayableTransactionError::Sending { + msg: "booga".to_string(), + hashes: vec![make_tx_hash(456)], + }), + response_skeleton_opt: None, + }; + let addr = subject.start(); + + addr.try_send(sent_payable.clone()) + .expect("unexpected actix error"); + + System::current().stop(); + assert_eq!(system.run(), 0); + let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); + let (actual_sent_payable, logger) = finish_scan_params.remove(0); + assert_eq!(actual_sent_payable, sent_payable,); + test_use_of_the_same_logger(&logger, test_name, None); + let mut payable_notify_later_params = payable_notify_later_params_arc.lock().unwrap(); + let (scheduled_msg, _interval) = payable_notify_later_params.remove(0); + assert_eq!(scheduled_msg, ScanForNewPayables::default()); + assert!( + payable_notify_later_params.is_empty(), + "Should be empty but {:?}", + payable_notify_later_params + ); + } + #[test] fn accountant_schedule_retry_payable_scanner_because_not_all_pending_payables_completed() { init_test_logging(); @@ -4140,7 +4242,7 @@ mod tests { response_skeleton_opt }] ); - test_use_of_the_same_logger(&logger, test_name) + test_use_of_the_same_logger(&logger, test_name, None) } #[test] @@ -5395,6 +5497,10 @@ mod tests { assert_on_initialization_with_panic_on_migration(&data_dir, &act); } + + fn bind_ui_gateway_unasserted(accountant: &mut Accountant) { + accountant.ui_message_sub_opt = Some(make_recorder().0.start().recipient()); + } } #[cfg(test)] diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index f2a845db1..20ffb1d89 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -28,7 +28,7 @@ use crate::sub_lib::accountant::{ }; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::wallet::Wallet; -use actix::{Message, Recipient}; +use actix::{Message}; use itertools::{Either, Itertools}; use masq_lib::logger::Logger; use masq_lib::logger::TIME_FORMATTING_STRING; @@ -38,18 +38,14 @@ use masq_lib::utils::ExpectValue; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; -use std::marker::PhantomData; use std::rc::Rc; -use std::sync::{Arc, RwLock}; use std::time::{SystemTime}; -use futures::future::result; use time::format_description::parse; use time::OffsetDateTime; use variant_count::VariantCount; use web3::types::H256; use crate::accountant::scanners::payable_scanner_extension::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; -use crate::accountant::scanners::scan_schedulers::{ScanSchedulers}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::db_config::persistent_configuration::{PersistentConfiguration, PersistentConfigurationReal}; @@ -274,8 +270,18 @@ impl Scanners { self.receivable.finish_scan(msg, logger) } - pub fn scan_error_scan_reset(&self, error: &ScanError) { - todo!("test me locally") + pub fn acknowledge_scan_error(&mut self, error: &ScanError, logger: &Logger) { + match error.scan_type { + ScanType::Payables => { + self.payable.mark_as_ended(logger); + } + ScanType::PendingPayables => { + self.pending_payable.mark_as_ended(logger); + } + ScanType::Receivables => { + self.receivable.mark_as_ended(logger); + } + }; } pub fn try_skipping_payable_adjustment( @@ -896,7 +902,7 @@ impl Scanner for PendingPay }) }; - match message.fingerprints_with_receipts.is_empty() { + let requires_payment_retry = match message.fingerprints_with_receipts.is_empty() { true => { debug!(logger, "No transaction receipts found."); todo!( @@ -911,17 +917,19 @@ impl Scanner for PendingPay message.fingerprints_with_receipts.len() ); let scan_report = self.handle_receipts_for_pending_transactions(message, logger); - let requires_payments_retry = + let requires_payment_retry = self.process_transactions_by_reported_state(scan_report, logger); self.mark_as_ended(&logger); - if requires_payments_retry { - todo!() - } else { - PendingPayableScanResult::NoPendingPayablesLeft(construct_msg_scan_ended_to_ui()) - } + requires_payment_retry } + }; + + if requires_payment_retry { + PendingPayableScanResult::PaymentRetryRequired + } else { + PendingPayableScanResult::NoPendingPayablesLeft(construct_msg_scan_ended_to_ui()) } } @@ -1366,10 +1374,8 @@ pub mod local_test_utils { MultistageDualPayableScanner, PreparedAdjustment, PrivateScanner, Scanner, SolvencySensitivePaymentInstructor, StartScanError, StartableScanner, }; + use crate::accountant::BlockchainAgentWithContextMessage; use crate::accountant::OutboundPaymentsInstructions; - use crate::accountant::{ - BlockchainAgentWithContextMessage, ScanForNewPayables, ScanForRetryPayables, - }; use crate::accountant::{ResponseSkeleton, SentPayables}; use crate::sub_lib::wallet::Wallet; use actix::{Message, System}; @@ -1664,9 +1670,9 @@ mod tests { use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult, PendingPayableMetadata}; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport, PendingPayableScanResult}; - use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, PrivateScanner}; + use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners}; use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; - use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanForNewPayables, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; + use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForNewPayables, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ @@ -1696,14 +1702,13 @@ mod tests { use std::ops::Sub; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::rc::Rc; - use std::sync::{Arc, Mutex, RwLock}; + use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; - use itertools::Either; use web3::types::{TransactionReceipt, H256}; use web3::Error; use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeToUiMessage; - use crate::accountant::scanners::local_test_utils::{NullScanner, ScannerMock}; + use crate::accountant::scanners::local_test_utils::{NullScanner}; use crate::accountant::scanners::test_utils::{MarkScanner, ReplacementType, ScannerReplacement}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; @@ -4435,6 +4440,72 @@ mod tests { ); } + #[test] + fn acknowledge_scan_error_works() { + fn scan_error(scan_type: ScanType) -> ScanError { + ScanError { + scan_type, + response_skeleton_opt: None, + msg: "bluh".to_string(), + } + } + + init_test_logging(); + let test_name = "acknowledge_scan_error_works"; + let inputs: Vec<( + ScanType, + Box, + Box Option>, + )> = vec![ + ( + ScanType::Payables, + Box::new(|subject| subject.payable.mark_as_started(SystemTime::now())), + Box::new(|subject| subject.payable.scan_started_at()), + ), + ( + ScanType::PendingPayables, + Box::new(|subject| subject.pending_payable.mark_as_started(SystemTime::now())), + Box::new(|subject| subject.pending_payable.scan_started_at()), + ), + ( + ScanType::Receivables, + Box::new(|subject| subject.receivable.mark_as_started(SystemTime::now())), + Box::new(|subject| subject.receivable.scan_started_at()), + ), + ]; + let mut subject = make_dull_subject(); + subject.payable = Box::new(PayableScannerBuilder::new().build()); + subject.pending_payable = Box::new(PendingPayableScannerBuilder::new().build()); + subject.receivable = Box::new(ReceivableScannerBuilder::new().build()); + let logger = Logger::new(test_name); + let test_log_handler = TestLogHandler::new(); + + inputs + .into_iter() + .for_each(|(scan_type, set_started, get_started_at)| { + set_started(&mut subject); + let started_at_before = get_started_at(&subject); + + subject.acknowledge_scan_error(&scan_error(scan_type), &logger); + + let started_at_after = get_started_at(&subject); + assert!( + started_at_before.is_some(), + "Should've been started for {:?}", + scan_type + ); + assert_eq!( + started_at_after, None, + "Should've been unset for {:?}", + scan_type + ); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: The {:?} scan ended in", + scan_type + )); + }) + } + fn make_dull_subject() -> Scanners { Scanners { payable: Box::new(NullScanner::new()), diff --git a/node/src/accountant/scanners/payable_scanner_extension/mod.rs b/node/src/accountant/scanners/payable_scanner_extension/mod.rs index 6ff3ebe20..649bc820f 100644 --- a/node/src/accountant/scanners/payable_scanner_extension/mod.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/mod.rs @@ -14,7 +14,6 @@ use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableS use crate::accountant::scanners::{Scanner, StartableScanner}; use crate::accountant::{ScanForNewPayables, ScanForRetryPayables, SentPayables}; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; -use actix::Message; use itertools::Either; use masq_lib::logger::Logger; diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 1dd5a7906..dcf83324a 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -12,7 +12,6 @@ use crate::sub_lib::utils::{ use actix::{Actor, Context, Handler}; use masq_lib::logger::Logger; use masq_lib::messages::ScanType; -use std::cell::RefCell; use std::fmt::Debug; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -145,14 +144,12 @@ impl PayableScanScheduler { pub struct PayableScanSchedulerInner { pub last_new_payable_scan_timestamp: SystemTime, - pub next_new_payable_scan_already_scheduled: bool, } impl Default for PayableScanSchedulerInner { fn default() -> Self { Self { last_new_payable_scan_timestamp: UNIX_EPOCH, - next_new_payable_scan_already_scheduled: false, } } } @@ -215,7 +212,7 @@ where debug!( logger, - "Scheduling a scan with {:?} in {}ms", + "Scheduling a scan via {:?} in {}ms", msg, self.interval.as_millis() ); @@ -362,10 +359,6 @@ mod tests { payable_scheduler_inner.last_new_payable_scan_timestamp, UNIX_EPOCH ); - assert_eq!( - payable_scheduler_inner.next_new_payable_scan_already_scheduled, - false - ); assert_eq!( schedulers.pending_payable.interval, scan_intervals.pending_payable_scan_interval @@ -586,7 +579,7 @@ mod tests { ALL_START_SCAN_ERRORS .iter() .enumerate() - .for_each(|(idx, (error))| { + .for_each(|(idx, error)| { let result = subject .reschedule_on_error_resolver .resolve_rescheduling_for_given_error(hintable_scanner, error, true); @@ -733,7 +726,7 @@ mod tests { #[test] fn resolve_rescheduling_for_given_error_works_for_retry_payables_if_externally_triggered() { - let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let subject = ScanSchedulers::new(ScanIntervals::default(), false); test_what_if_externally_triggered(&subject, HintableScanner::RetryPayables {}); } @@ -757,7 +750,7 @@ mod tests { let panic_msg = panic.downcast_ref::().unwrap(); let expected_msg = format!( "internal error: entered unreachable code: {:?} should be impossible \ - with RetryPayablesScanner in automatic mode.", + with RetryPayableScanner in automatic mode", error ); assert_eq!( diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 397cd434f..e837a7f0a 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -6,7 +6,7 @@ pub mod payable_scanner_utils { use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; - use crate::accountant::{comma_joined_stringifiable, ResponseSkeleton, SentPayables}; + use crate::accountant::{comma_joined_stringifiable, SentPayables}; use crate::sub_lib::accountant::PaymentThresholds; use crate::sub_lib::wallet::Wallet; use itertools::Itertools; diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index a916e2dd2..c75454568 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -12,7 +12,6 @@ use crate::accountant::{ ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, SentPayables, }; use crate::blockchain::blockchain_bridge::RetrieveTransactions; -use itertools::Either; use masq_lib::logger::Logger; use masq_lib::ui_gateway::NodeToUiMessage; use std::cell::RefCell; diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 0c2b3de5a..6d308def3 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -1017,11 +1017,11 @@ pub mod unshared_test_utils { if let Some(remaining) = self.stop_system_on_count_received_opt.borrow_mut().as_mut() { + *remaining -= 1; if remaining == &0 { System::current().stop(); return; } - *remaining -= 1; } if self.send_message_out { ctx.notify(msg) diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index 806328883..7e3a2d883 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -28,7 +28,6 @@ use crate::sub_lib::hopper::{HopperSubs, MessageType}; use crate::sub_lib::neighborhood::NeighborhoodSubs; use crate::sub_lib::neighborhood::{ConfigChangeMsg, ConnectionProgressMessage}; -use crate::proxy_server::ProxyServer; use crate::sub_lib::configurator::ConfiguratorSubs; use crate::sub_lib::neighborhood::NodeQueryResponseMetadata; use crate::sub_lib::neighborhood::RemoveNeighborMessage; From c3bcd285e463df196120cd545edc1d7599951f49 Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 25 May 2025 19:02:50 +0200 Subject: [PATCH 29/49] GH-602: one more test written...ext tr. pen payable --- node/src/accountant/mod.rs | 68 +++++++++++++++++++++++++++-- node/src/accountant/scanners/mod.rs | 2 +- node/src/test_utils/mod.rs | 15 ++++++- 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index c65265058..1f3772ef2 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -291,7 +291,6 @@ impl Handler for Accountant { .expect("UIGateway is not bound") .try_send(node_to_ui_msg) .expect("UIGateway is dead"); - todo!("Finishing PendingPayable scan. Non-automatic."); // Externally triggered scan is not allowed to be a spark for a procedure that // would involve payables with fresh nonces. The job is done. } else { @@ -1815,7 +1814,6 @@ mod tests { #[test] fn externally_triggered_scan_pending_payables_request() { let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - config.automatic_scans_enabled = false; config.scan_intervals_opt = Some(ScanIntervals { payable_scan_interval: Duration::from_millis(10_000), receivable_scan_interval: Duration::from_millis(10_000), @@ -1873,6 +1871,68 @@ mod tests { ); } + #[test] + fn externally_triggered_scan_identifies_all_pending_payables_as_complete() { + let transaction_confirmed_params_arc = Arc::new(Mutex::new(vec![])); + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 565, + context_id: 112233, + }); + let payable_dao = PayableDaoMock::default() + .transactions_confirmed_params(&transaction_confirmed_params_arc) + .transactions_confirmed_result(Ok(())); + let pending_payable_dao = + PendingPayableDaoMock::default().delete_fingerprints_result(Ok(())); + let mut subject = AccountantBuilder::default() + .payable_daos(vec![ForPendingPayableScanner(payable_dao)]) + .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) + .build(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let ui_gateway_addr = ui_gateway.start(); + let system = System::new("test"); + subject.scan_schedulers.automatic_scans_enabled = false; + // Making sure we would kill the test if any sort of scan was scheduled + subject.scan_schedulers.payable.retry_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify_later = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().panic_on_schedule_attempt()); + subject.ui_message_sub_opt = Some(ui_gateway_addr.recipient()); + let subject_addr = subject.start(); + let tx_fingerprint = make_pending_payable_fingerprint(); + let report_tx_receipts = ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash: make_tx_hash(777), + status: TxStatus::Succeeded(TransactionBlock { + block_hash: make_tx_hash(456), + block_number: 78901234.into(), + }), + }), + tx_fingerprint.clone(), + )], + response_skeleton_opt, + }; + + subject_addr.try_send(report_tx_receipts).unwrap(); + + system.run(); + let transaction_confirmed_params = transaction_confirmed_params_arc.lock().unwrap(); + assert_eq!(*transaction_confirmed_params, vec![vec![tx_fingerprint]]); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + assert_eq!( + ui_gateway_recording.get_record::(0), + &NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton_opt.unwrap().client_id), + body: UiScanResponse {}.tmb(response_skeleton_opt.unwrap().context_id), + } + ); + assert_eq!(ui_gateway_recording.len(), 1); + } + #[test] fn externally_triggered_scan_is_not_handled_in_case_the_scan_is_already_running() { init_test_logging(); @@ -2018,8 +2078,8 @@ mod tests { ) { let test_name = "externally_triggered_scan_for_pending_payables_is_prevented_if_all_payments_already_complete"; let expected_log_msg = format!( - "INFO: {test_name}: Manual PendingPayables scan was \ - denied for a predictable zero effect. Run the Payable scanner first." + "INFO: {test_name}: Manual PendingPayables scan was denied for the predictable zero \ + effect. Run the Payable scanner first." ); test_externally_triggered_scan_is_prevented_if( diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 20ffb1d89..c7ae3e30c 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1314,7 +1314,7 @@ impl StartScanError { scan_type )), MTError::UnnecessaryRequest { hint_opt } => ErrorType::Temporary(format!( - "Manual {:?} scan was denied for a predictable zero effect.{}", + "Manual {:?} scan was denied for the predictable zero effect.{}", scan_type, match hint_opt { Some(hint) => format!(" {}", hint), diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 6d308def3..b36199b75 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -972,6 +972,7 @@ pub mod unshared_test_utils { notify_params: Arc>>, send_message_out: bool, stop_system_on_count_received_opt: RefCell>, + panic_on_schedule_attempt: bool, } impl Default for NotifyHandleMock { @@ -980,6 +981,7 @@ pub mod unshared_test_utils { notify_params: Arc::new(Mutex::new(vec![])), send_message_out: false, stop_system_on_count_received_opt: RefCell::new(None), + panic_on_schedule_attempt: false, } } } @@ -1005,14 +1007,25 @@ pub mod unshared_test_utils { .replace(Some(msg_count)); self } + + pub fn panic_on_schedule_attempt(mut self) -> Self { + self.panic_on_schedule_attempt = true; + self + } } impl NotifyHandle for NotifyHandleMock where - M: Message + 'static + Clone, + M: Message + Debug + Clone + 'static, A: Actor> + Handler, { fn notify<'a>(&'a self, msg: M, ctx: &'a mut Context) { + if self.panic_on_schedule_attempt { + panic!( + "Message scheduling request for {:?}, thought not expected", + msg + ) + } self.notify_params.lock().unwrap().push(msg.clone()); if let Some(remaining) = self.stop_system_on_count_received_opt.borrow_mut().as_mut() From b2966131cee958eac1bceca6c642cee7821a77e9 Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 25 May 2025 20:26:22 +0200 Subject: [PATCH 30/49] GH-602: eliminated last todos in scanners/mod.rs --- node/src/accountant/mod.rs | 90 +++++++++++++--------- node/src/accountant/scanners/mod.rs | 112 ++++++++++++++++++++++++++-- 2 files changed, 161 insertions(+), 41 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 1f3772ef2..dd62d2fa6 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -937,7 +937,7 @@ impl Accountant { response_skeleton_opt, &self.logger, ), - None => todo!("I need a test here"), //Err(StartScanError::NoConsumingWalletFound), + None => Err(StartScanError::NoConsumingWalletFound), }; match result { @@ -949,35 +949,11 @@ impl Accountant { .expect("BlockchainBridge is dead"); SchedulingAfterHaltedScan::DoNotSchedule } - Err(e) => { - self.handle_start_scan_error_for_scanner_with_irregular_scheduling( - todo!(), - e, - response_skeleton_opt, - ) - // let is_externally_triggered = response_skeleton_opt.is_some(); - // todo!("process...and eventually schedule for pending payable"); - // // e.log_error( - // // &self.logger, - // // ScanType::Payables, - // // is_externally_triggered, - // // ); - // // - // // response_skeleton_opt.map(|skeleton| { - // // self.ui_message_sub_opt - // // .as_ref() - // // .expect("UiGateway is unbound") - // // .try_send(NodeToUiMessage { - // // target: MessageTarget::ClientId(skeleton.client_id), - // // body: UiScanResponse {}.tmb(skeleton.context_id), - // // }) - // // .expect("UiGateway is dead"); - // // }); - // - // self.scan_schedulers - // .reschedule_on_error_resolver - // .resolve_rescheduling_for_given_error(todo!(), &e, is_externally_triggered) - } + Err(e) => self.handle_start_scan_error_for_scanner_with_irregular_scheduling( + HintableScanner::RetryPayables, + e, + response_skeleton_opt, + ), } } @@ -2311,10 +2287,6 @@ mod tests { subject .scanners .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); - let response_skeleton_opt = Some(ResponseSkeleton { - client_id: 789, - context_id: 111, - }); let accountant_addr = subject.start(); let accountant_subs = Accountant::make_subs_from(&accountant_addr); let peer_actors = peer_actors_builder() @@ -2324,7 +2296,7 @@ mod tests { accountant_addr .try_send(ScanForRetryPayables { - response_skeleton_opt, + response_skeleton_opt: None, }) .unwrap(); @@ -2336,7 +2308,7 @@ mod tests { let (actual_wallet, actual_now, actual_response_skeleton_opt, actual_logger, _) = start_scan_params.remove(0); assert_eq!(actual_wallet, consuming_wallet); - assert_eq!(actual_response_skeleton_opt, response_skeleton_opt); + assert_eq!(actual_response_skeleton_opt, None); assert!(before <= actual_now && actual_now <= after); assert!( start_scan_params.is_empty(), @@ -2350,6 +2322,52 @@ mod tests { test_use_of_the_same_logger(&actual_logger, test_name, None) } + #[test] + fn scan_for_retry_payables_if_consuming_wallet_is_not_present() { + init_test_logging(); + let test_name = "scan_for_retry_payables_if_consuming_wallet_is_not_present"; + let system = System::new(test_name); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway = + ui_gateway.system_stop_conditions(match_lazily_every_type_id!(NodeToUiMessage)); + let ui_gateway_addr = ui_gateway.start(); + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .build(); + let payable_scanner_mock = ScannerMock::new(); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner_mock, + ))); + subject.ui_message_sub_opt = Some(ui_gateway_addr.recipient()); + // It must be populated because no errors are tolerated at the RetryPayableScanner + // if automatic scans are on + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 789, + context_id: 111, + }); + let accountant_addr = subject.start(); + + accountant_addr + .try_send(ScanForRetryPayables { + response_skeleton_opt, + }) + .unwrap(); + + system.run(); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let message = ui_gateway_recording.get_record::(0); + assert_eq!( + message, + &NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton_opt.unwrap().client_id), + body: UiScanResponse {}.tmb(response_skeleton_opt.unwrap().context_id) + } + ); + TestLogHandler::new().exists_log_containing(&format!("WARN: {test_name}: Cannot initiate Payables scan because no consuming wallet was found")); + } + #[test] fn accountant_requests_blockchain_bridge_to_scan_for_received_payments() { init_test_logging(); diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index c7ae3e30c..4d922a0d4 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1305,20 +1305,20 @@ impl StartScanError { scan_type )), StartScanError::CalledFromNullScanner => match cfg!(test) { - true => todo!(), //None, + true => ErrorType::Permanent(format!("Called from NullScanner, not the {:?} scanner.", scan_type)), false => panic!("Null Scanner shouldn't be running inside production code."), }, StartScanError::ManualTriggerError(e) => match e { MTError::AutomaticScanConflict => ErrorType::Permanent(format!( - "Manual {:?} scan was denied. Automatic mode prevents manual triggers.", + "Requested manually, the {:?} scan was denied. Automatic mode prevents manual triggers.", scan_type )), MTError::UnnecessaryRequest { hint_opt } => ErrorType::Temporary(format!( - "Manual {:?} scan was denied for the predictable zero effect.{}", + "Requested manually, the {:?} scan was denied expecting zero findings.{}", scan_type, match hint_opt { Some(hint) => format!(" {}", hint), - None => todo!(), + None => "".to_string(), } )), }, @@ -1670,7 +1670,7 @@ mod tests { use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult, PendingPayableMetadata}; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport, PendingPayableScanResult}; - use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners}; + use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, MTError}; use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForNewPayables, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; @@ -4506,6 +4506,108 @@ mod tests { }) } + #[test] + fn log_error_works_fine_for_automatic_scanning() { + init_test_logging(); + let test_name = "log_error_works_fine_for_automatic_scanning"; + let input = vec![ + ( + StartScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Payables, + started_at: SystemTime::now() + }, + format!("DEBUG: {test_name}: Payables scan was already initiated at" /*TODO suboptimal */) + ), + ( + StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), + format!("DEBUG: {test_name}: Requested manually, the Payables scan was denied. Automatic mode prevents manual triggers.") + ), + ( + StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { + hint_opt: Some("Wise words".to_string()) + }), + format!("DEBUG: {test_name}: Requested manually, the Payables scan was denied expecting zero findings. Wise words") + ), + ( + StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { + hint_opt: None} + ), + format!("DEBUG: {test_name}: Requested manually, the Payables scan was denied expecting zero findings.") + ), + ( + StartScanError::CalledFromNullScanner, + format!("DEBUG: {test_name}: Called from NullScanner, not the Payables scanner.") + ), + ( + StartScanError::NoConsumingWalletFound, + format!("DEBUG: {test_name}: Cannot initiate Payables scan because no consuming wallet was found.") + ), + ( + StartScanError::NothingToProcess, + format!("DEBUG: {test_name}: There was nothing to process during Payables scan.") + ), + ]; + let logger = Logger::new(test_name); + let test_log_handler = TestLogHandler::new(); + + input.into_iter().for_each(|(err, expected_log_msg)| { + err.log_error(&logger, ScanType::Payables, false); + + test_log_handler.exists_log_containing(&expected_log_msg); + }); + } + + #[test] + fn log_error_works_fine_for_externally_triggered_scanning() { + init_test_logging(); + let test_name = "log_error_works_fine_for_externally_triggered_scanning"; + let input = vec![ + ( + StartScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Payables, + started_at: SystemTime::now() + }, + format!("INFO: {test_name}: Payables scan was already initiated at" /*TODO suboptimal */) + ), + ( + StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), + format!("WARN: {test_name}: Requested manually, the Payables scan was denied. Automatic mode prevents manual triggers.") + ), + ( + StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { + hint_opt: Some("Wise words".to_string()) + }), + format!("INFO: {test_name}: Requested manually, the Payables scan was denied expecting zero findings. Wise words") + ), + ( + StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { + hint_opt: None} + ), + format!("INFO: {test_name}: Requested manually, the Payables scan was denied expecting zero findings.") + ), + ( + StartScanError::CalledFromNullScanner, + format!("WARN: {test_name}: Called from NullScanner, not the Payables scanner.") + ), + ( + StartScanError::NoConsumingWalletFound, + format!("WARN: {test_name}: Cannot initiate Payables scan because no consuming wallet was found.") + ), + ( + StartScanError::NothingToProcess, + format!("INFO: {test_name}: There was nothing to process during Payables scan.") + ), + ]; + let logger = Logger::new(test_name); + let test_log_handler = TestLogHandler::new(); + + input.into_iter().for_each(|(err, expected_log_msg)| { + err.log_error(&logger, ScanType::Payables, true); + + test_log_handler.exists_log_containing(&expected_log_msg); + }); + } + fn make_dull_subject() -> Scanners { Scanners { payable: Box::new(NullScanner::new()), From c69c1d615bf9284efc103a9dbe75f27ce6198566 Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 25 May 2025 20:56:11 +0200 Subject: [PATCH 31/49] GH-602: first rough clean-up --- node/src/accountant/mod.rs | 6 +++--- node/src/accountant/scanners/mod.rs | 21 ++++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index dd62d2fa6..5063c503a 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1971,7 +1971,7 @@ mod tests { scan_type: ScanType, ) { let expected_log_msg = format!( - "WARN: {test_name}: Manual {:?} scan was denied. Automatic mode \ + "WARN: {test_name}: User requested {:?} scan was denied. Automatic mode \ prevents manual triggers.", scan_type ); @@ -2054,8 +2054,8 @@ mod tests { ) { let test_name = "externally_triggered_scan_for_pending_payables_is_prevented_if_all_payments_already_complete"; let expected_log_msg = format!( - "INFO: {test_name}: Manual PendingPayables scan was denied for the predictable zero \ - effect. Run the Payable scanner first." + "INFO: {test_name}: User requested PendingPayables scan was denied expecting zero \ + findings. Run the Payable scanner first." ); test_externally_triggered_scan_is_prevented_if( diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 4d922a0d4..557594daf 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1305,16 +1305,19 @@ impl StartScanError { scan_type )), StartScanError::CalledFromNullScanner => match cfg!(test) { - true => ErrorType::Permanent(format!("Called from NullScanner, not the {:?} scanner.", scan_type)), + true => ErrorType::Permanent(format!( + "Called from NullScanner, not the {:?} scanner.", + scan_type + )), false => panic!("Null Scanner shouldn't be running inside production code."), }, StartScanError::ManualTriggerError(e) => match e { MTError::AutomaticScanConflict => ErrorType::Permanent(format!( - "Requested manually, the {:?} scan was denied. Automatic mode prevents manual triggers.", + "User requested {:?} scan was denied. Automatic mode prevents manual triggers.", scan_type )), MTError::UnnecessaryRequest { hint_opt } => ErrorType::Temporary(format!( - "Requested manually, the {:?} scan was denied expecting zero findings.{}", + "User requested {:?} scan was denied expecting zero findings.{}", scan_type, match hint_opt { Some(hint) => format!(" {}", hint), @@ -4520,19 +4523,19 @@ mod tests { ), ( StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), - format!("DEBUG: {test_name}: Requested manually, the Payables scan was denied. Automatic mode prevents manual triggers.") + format!("DEBUG: {test_name}: User requested Payables scan was denied. Automatic mode prevents manual triggers.") ), ( StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { hint_opt: Some("Wise words".to_string()) }), - format!("DEBUG: {test_name}: Requested manually, the Payables scan was denied expecting zero findings. Wise words") + format!("DEBUG: {test_name}: User requested Payables scan was denied expecting zero findings. Wise words") ), ( StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { hint_opt: None} ), - format!("DEBUG: {test_name}: Requested manually, the Payables scan was denied expecting zero findings.") + format!("DEBUG: {test_name}: User requested Payables scan was denied expecting zero findings.") ), ( StartScanError::CalledFromNullScanner, @@ -4571,19 +4574,19 @@ mod tests { ), ( StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), - format!("WARN: {test_name}: Requested manually, the Payables scan was denied. Automatic mode prevents manual triggers.") + format!("WARN: {test_name}: User requested Payables scan was denied. Automatic mode prevents manual triggers.") ), ( StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { hint_opt: Some("Wise words".to_string()) }), - format!("INFO: {test_name}: Requested manually, the Payables scan was denied expecting zero findings. Wise words") + format!("INFO: {test_name}: User requested Payables scan was denied expecting zero findings. Wise words") ), ( StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { hint_opt: None} ), - format!("INFO: {test_name}: Requested manually, the Payables scan was denied expecting zero findings.") + format!("INFO: {test_name}: User requested Payables scan was denied expecting zero findings.") ), ( StartScanError::CalledFromNullScanner, From 6e1c14778dd5d2717f92f45ba2b166a3f8ee04fe Mon Sep 17 00:00:00 2001 From: Bert Date: Sun, 25 May 2025 21:42:45 +0200 Subject: [PATCH 32/49] GH-602: some comments fixed --- node/src/accountant/mod.rs | 38 ++++++++++++++--------------- node/src/accountant/scanners/mod.rs | 6 ++--- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 5063c503a..587e6f18b 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -220,10 +220,10 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { - // By this time we know it is an automatic scanner, which may or may not be rescheduled. - // It depends on the findings: if it finds failed transactions, then it will launch - // the RetryPayableScanner, which finishes, and the PendingPayablesScanner is scheduled - // to run again. Therefore, not from here. + // By now we know this is an automatic scan process. The scan may be or may not be + // rescheduled. It depends on the findings. Any failed transactions will lead to the launch + // of the RetryPayableScanner, which finishes, and the PendingPayablesScanner is scheduled + // to run again. However, not from here. let response_skeleton_opt = msg.response_skeleton_opt; if let SchedulingAfterHaltedScan::Schedule(ScanType::Payables) = self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) @@ -239,12 +239,11 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForNewPayables, ctx: &mut Self::Context) -> Self::Result { - // TODO recheck these descriptions - // We know this must be a scheduled scanner, but we don't if we are going to reschedule it - // from here or elsewhere. If the scan finds no payables that qualify for a payment, we do - // it right away. If some new pending payables are produced, the next scheduling is going to - // be determined by the PendingPayableScanner, evaluating if it has seen all pending - // payables complete. That opens up an opportunity for another run of the NewPayableScanner. + // We know this must be a scheduled scan, but are yet clueless if where it's going to be + // rescheduled. If no payables qualify for a payment, we do it here right away. If some + // transactions made it out, the next scheduling of this scanner is going to be decided by + // the PendingPayableScanner whose job is to evaluate if it has seen every pending payable + // complete. That's the moment when another run of the NewPayableScanner makes sense again. let response_skeleton = msg.response_skeleton_opt; if let SchedulingAfterHaltedScan::Schedule(ScanType::Payables) = self.handle_request_of_scan_for_new_payable(response_skeleton) @@ -260,8 +259,8 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForRetryPayables, _ctx: &mut Self::Context) -> Self::Result { - // RetryPayableScanner is scheduled only when the PendingPayableScanner finishes finding out - // that there have been some failed pending payables. That means not from here. + // RetryPayableScanner is scheduled only when the PendingPayableScanner finishes discovering + // that there have been some failed pending payables. No place for it here. let response_skeleton = msg.response_skeleton_opt; let _ = self.handle_request_of_scan_for_retry_payable(response_skeleton); } @@ -271,8 +270,8 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { - // By this time we know it is an automatic scanner, which is always rescheduled right away, - // no matter what its outcome is. + // By now we know it is an automatic scan, which, for this particular scanner, is + // rescheduled pretty regularly, right here, no matter what its outcome is. self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); self.scan_schedulers.receivable.schedule(ctx, &self.logger); } @@ -291,8 +290,8 @@ impl Handler for Accountant { .expect("UIGateway is not bound") .try_send(node_to_ui_msg) .expect("UIGateway is dead"); - // Externally triggered scan is not allowed to be a spark for a procedure that - // would involve payables with fresh nonces. The job is done. + // Externally triggered scan should never be allowed to spark a procedure that + // would bring around payables with fresh nonces. The job's done. } else { self.scan_schedulers .payable @@ -343,10 +342,9 @@ impl Handler for Accountant { .try_send(node_to_ui_msg) .expect("UIGateway is dead"); - // When automatic scans are suppressed, the external triggers are not allowed to - // provoke an unwinding scan sequence across intervals. The only exception is - // the PendingPayableScanner and RetryPayableScanner, which are meant to run in - // a tight tandem. + // Externally triggered scans are not allowed to provoke an unwinding scan sequence + // with intervals. The only exception is the PendingPayableScanner and + // RetryPayableScanner, which are ever meant to run in a tight tandem. } } } diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 557594daf..4f08d5a93 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -146,8 +146,8 @@ impl Scanners { ) } - // Note: This scanner cannot be started on its own, it always comes after the pending payable - // scan, but only if it was made clear that there is a need to perform this retry. + // Note: This scanner cannot be started on its own. It always runs after the pending payable + // scan, but only if it is clear that a retry is needed. pub fn start_retry_payable_scan_guarded( &mut self, wallet: &Wallet, @@ -192,7 +192,7 @@ impl Scanners { self.payable.scan_started_at(), ) { (Some(pp_timestamp), Some(p_timestamp)) => - // If you're wondering, then yes, this should be the sacre truth between + // If you're wondering, then yes, this condition should be the sacre truth between // PendingPayableScanner and NewPayableScanner. { unreachable!( From e5d2249693947d823fff84bd65d57d06144ffa1d Mon Sep 17 00:00:00 2001 From: Bert Date: Mon, 2 Jun 2025 20:24:57 +0200 Subject: [PATCH 33/49] GH-602: first piece of the review finished --- .../tests/verify_bill_payment.rs | 2 +- node/src/accountant/mod.rs | 2 +- .../src/accountant/scanners/scanners_utils.rs | 108 +++--- node/src/accountant/scanners/test_utils.rs | 31 +- node/src/accountant/test_utils.rs | 8 +- node/src/blockchain/blockchain_bridge.rs | 1 - node/src/daemon/setup_reporter.rs | 3 + node/src/proxy_server/mod.rs | 2 +- node/src/sub_lib/combined_parameters.rs | 5 +- node/src/sub_lib/utils.rs | 4 +- node/src/test_utils/recorder.rs | 341 ++++++++++-------- node/src/test_utils/recorder_counter_msgs.rs | 50 +-- .../test_utils/recorder_stop_conditions.rs | 16 +- 13 files changed, 318 insertions(+), 255 deletions(-) diff --git a/multinode_integration_tests/tests/verify_bill_payment.rs b/multinode_integration_tests/tests/verify_bill_payment.rs index 34df5dbca..5d682fea4 100644 --- a/multinode_integration_tests/tests/verify_bill_payment.rs +++ b/multinode_integration_tests/tests/verify_bill_payment.rs @@ -432,7 +432,7 @@ fn verify_pending_payables() { ); ui_client.send_request( UiScanRequest { - scan_type: ScanType::Payables, + scan_type: ScanType::PendingPayables, } .tmb(0), ); diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 6755704bd..370ed6080 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1262,7 +1262,7 @@ mod tests { use crate::accountant::scanners::scan_schedulers::{NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; - use crate::test_utils::recorder_counter_msgs::SingleCounterMsgSetup; + use crate::test_utils::recorder_counter_msgs::SingleTypeCounterMsgSetup; impl Handler> for Accountant { type Result = (); diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index b3770407c..4d2bf16e1 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -337,7 +337,7 @@ pub mod pending_payable_scanner_utils { impl PendingPayableScanReport { pub fn requires_payments_retry(&self) -> bool { - !self.still_pending.is_empty() || !self.failures.is_empty() + todo!("complete my within GH-642") } } @@ -478,7 +478,7 @@ mod tests { PayableThresholdsGaugeReal, }; use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; - use crate::accountant::{checked_conversion, gwei_to_wei, PendingPayableId, SentPayables}; + use crate::accountant::{checked_conversion, gwei_to_wei, SentPayables}; use crate::blockchain::test_utils::make_tx_hash; use crate::sub_lib::accountant::PaymentThresholds; use crate::test_utils::make_wallet; @@ -487,8 +487,6 @@ mod tests { use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use std::time::SystemTime; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; - use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanReport; - use crate::accountant::test_utils::make_pending_payable_fingerprint; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RpcPayableFailure}; @@ -874,59 +872,61 @@ mod tests { #[test] fn requires_payments_retry_says_yes() { - let cases = vec![ - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - failures: vec![], - confirmed: vec![], - }, - PendingPayableScanReport { - still_pending: vec![], - failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - confirmed: vec![], - }, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - confirmed: vec![], - }, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - confirmed: vec![make_pending_payable_fingerprint()], - }, - PendingPayableScanReport { - still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], - failures: vec![], - confirmed: vec![make_pending_payable_fingerprint()], - }, - PendingPayableScanReport { - still_pending: vec![], - failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], - confirmed: vec![make_pending_payable_fingerprint()], - }, - ]; - - cases.into_iter().enumerate().for_each(|(idx, case)| { - let result = case.requires_payments_retry(); - assert_eq!( - result, true, - "We expected true, but got false for case of idx {}", - idx - ) - }) + todo!("complete this test with GH-604") + // let cases = vec![ + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // PendingPayableScanReport { + // still_pending: vec![PendingPayableId::new(12, make_tx_hash(456))], + // failures: vec![], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![PendingPayableId::new(456, make_tx_hash(1234))], + // confirmed: vec![make_pending_payable_fingerprint()], + // }, + // ]; + // + // cases.into_iter().enumerate().for_each(|(idx, case)| { + // let result = case.requires_payments_retry(); + // assert_eq!( + // result, true, + // "We expected true, but got false for case of idx {}", + // idx + // ) + // }) } #[test] fn requires_payments_retry_says_no() { - let report = PendingPayableScanReport { - still_pending: vec![], - failures: vec![], - confirmed: vec![make_pending_payable_fingerprint()], - }; - - let result = report.requires_payments_retry(); - - assert_eq!(result, false) + todo!("complete this test with GH-604") + // let report = PendingPayableScanReport { + // still_pending: vec![], + // failures: vec![], + // confirmed: vec![make_pending_payable_fingerprint()], + // }; + // + // let result = report.requires_payments_retry(); + // + // assert_eq!(result, false) } } diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index c75454568..878e33465 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -55,37 +55,34 @@ impl NewPayableScanDynIntervalComputerMock { } } -pub enum ReplacementType { - Real(A), - Mock(B), +pub enum ReplacementType { + Real(ScannerReal), + Mock(ScannerMock), Null, } -// The supplied scanner types are broken down to these detailed categories because they are -// eventually represented by a private trait within the Scanners struct. Therefore, when -// the values are constructed, they cannot be made into a trait object right away and needs to be -// handled specifically. +// The supplied scanner types are broken down to these detailed categories because they become +// eventually trait objects represented by a private trait. That one cannot be supplied, though, +// because it is unknown to the outside world, therefore, we provide the specific objects that +// will be then treated in an abstract manner. pub enum ScannerReplacement { Payable( - ReplacementType< - PayableScanner, - ScannerMock, - >, + ReplacementType, ), PendingPayable( ReplacementType< PendingPayableScanner, - ScannerMock< - RequestTransactionReceipts, - ReportTransactionReceipts, - PendingPayableScanResult, - >, + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, >, ), Receivable( ReplacementType< ReceivableScanner, - ScannerMock>, + RetrieveTransactions, + ReceivedPayments, + Option, >, ), } diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 262962b27..e0e5a6cdd 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -120,7 +120,7 @@ pub enum DaoWithDestination { enum DestinationMarker { AccountantBody, PendingPayableScanner, - SharedPayableScanner, + PayableScanner, ReceivableScanner, } @@ -132,7 +132,7 @@ impl DaoWithDestination { matches!(dest_marker, DestinationMarker::PendingPayableScanner) } Self::ForPayableScanner(_) => { - matches!(dest_marker, DestinationMarker::SharedPayableScanner) + matches!(dest_marker, DestinationMarker::PayableScanner) } Self::ForReceivableScanner(_) => { matches!(dest_marker, DestinationMarker::ReceivableScanner) @@ -237,13 +237,13 @@ macro_rules! create_or_update_factory { const PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::AccountantBody, - DestinationMarker::SharedPayableScanner, + DestinationMarker::PayableScanner, DestinationMarker::PendingPayableScanner, ]; const PENDING_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ DestinationMarker::AccountantBody, - DestinationMarker::SharedPayableScanner, + DestinationMarker::PayableScanner, DestinationMarker::PendingPayableScanner, ]; diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 27af51981..5b2fcccaa 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -572,7 +572,6 @@ mod tests { use crate::test_utils::recorder::{ make_accountant_subs_from_recorder, make_recorder, peer_actors_builder, }; - use crate::test_utils::recorder_stop_conditions::MsgIdentification; use crate::test_utils::recorder_stop_conditions::StopConditions; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; use crate::test_utils::unshared_test_utils::{ diff --git a/node/src/daemon/setup_reporter.rs b/node/src/daemon/setup_reporter.rs index 9c5ba7e00..fd051e3e1 100644 --- a/node/src/daemon/setup_reporter.rs +++ b/node/src/daemon/setup_reporter.rs @@ -3443,6 +3443,9 @@ mod tests { scan_intervals.payable_scan_interval = scan_intervals .payable_scan_interval .add(Duration::from_secs(15)); + scan_intervals.pending_payable_scan_interval = scan_intervals + .pending_payable_scan_interval + .add(Duration::from_secs(20)); scan_intervals.receivable_scan_interval = scan_intervals .receivable_scan_interval .sub(Duration::from_secs(33)); diff --git a/node/src/proxy_server/mod.rs b/node/src/proxy_server/mod.rs index 077ea8ae5..373688139 100644 --- a/node/src/proxy_server/mod.rs +++ b/node/src/proxy_server/mod.rs @@ -1380,7 +1380,7 @@ mod tests { use crate::test_utils::recorder::make_recorder; use crate::test_utils::recorder::peer_actors_builder; use crate::test_utils::recorder::Recorder; - use crate::test_utils::recorder_stop_conditions::{MsgIdentification, StopConditions}; + use crate::test_utils::recorder_stop_conditions::StopConditions; use crate::test_utils::unshared_test_utils::{ make_request_payload, prove_that_crash_request_handler_is_hooked_up, AssertionsMessage, }; diff --git a/node/src/sub_lib/combined_parameters.rs b/node/src/sub_lib/combined_parameters.rs index e76d7cf78..53a3e8488 100644 --- a/node/src/sub_lib/combined_parameters.rs +++ b/node/src/sub_lib/combined_parameters.rs @@ -224,8 +224,9 @@ impl Display for ScanIntervals { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "{}|{}", + "{}|{}|{}", self.payable_scan_interval.as_secs(), + self.pending_payable_scan_interval.as_secs(), self.receivable_scan_interval.as_secs() ) } @@ -399,8 +400,8 @@ mod tests { assert_eq!( scan_interval, &[ - ("pending_payable_scan_interval", U64), ("payable_scan_interval", U64), + ("pending_payable_scan_interval", U64), ("receivable_scan_interval", U64), ] ); diff --git a/node/src/sub_lib/utils.rs b/node/src/sub_lib/utils.rs index bbef4d3ec..5bd7a655a 100644 --- a/node/src/sub_lib/utils.rs +++ b/node/src/sub_lib/utils.rs @@ -139,7 +139,7 @@ fn crash_request_analyzer( } } -pub trait NotifyLaterHandle: Send +pub trait NotifyLaterHandle where A: Actor>, { @@ -167,7 +167,7 @@ impl NotifyLaterHandleReal { impl NotifyLaterHandle for NotifyLaterHandleReal where - M: Message + 'static + Send, + M: Message + 'static, A: Actor> + Handler, { fn notify_later( diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index 7e3a2d883..33c023197 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -3,11 +3,11 @@ use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; -use crate::accountant::ReportTransactionReceipts; use crate::accountant::{ ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForNewPayables, ScanForReceivables, SentPayables, }; +use crate::accountant::{ReportTransactionReceipts, ScanForPendingPayables, ScanForRetryPayables}; use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::daemon::crash_notification::CrashNotification; @@ -48,7 +48,7 @@ use crate::sub_lib::stream_handler_pool::TransmitDataMsg; use crate::sub_lib::ui_gateway::UiGatewaySubs; use crate::sub_lib::utils::MessageScheduler; use crate::test_utils::recorder_counter_msgs::{ - CounterMessages, CounterMsgGear, SingleCounterMsgSetup, + CounterMessages, CounterMsgGear, SingleTypeCounterMsgSetup, }; use crate::test_utils::recorder_stop_conditions::{ ForcedMatchable, MsgIdentification, PretendedMatchableWrapper, StopConditions, @@ -73,7 +73,7 @@ pub struct Recorder { recording: Arc>, node_query_responses: Vec>, route_query_responses: Vec>, - expected_future_counter_msgs_opt: Option, + counter_msgs_opt: Option, stop_conditions_opt: Option, } @@ -105,7 +105,7 @@ macro_rules! message_handler_common { macro_rules! matchable { ($message_type: ty) => { impl ForcedMatchable<$message_type> for $message_type { - fn correct_msg_type_id(&self) -> TypeId { + fn trigger_msg_type_id(&self) -> TypeId { TypeId::of::<$message_type>() } } @@ -167,6 +167,8 @@ recorder_message_handler_t_m_p!(RequestTransactionReceipts); recorder_message_handler_t_m_p!(RetrieveTransactions); recorder_message_handler_t_m_p!(ScanError); recorder_message_handler_t_m_p!(ScanForNewPayables); +recorder_message_handler_t_m_p!(ScanForRetryPayables); +recorder_message_handler_t_m_p!(ScanForPendingPayables); recorder_message_handler_t_m_p!(ScanForReceivables); recorder_message_handler_t_m_p!(SentPayables); recorder_message_handler_t_m_p!(StartMessage); @@ -190,7 +192,7 @@ where OuterM: PartialEq + 'static, InnerM: PartialEq + Send + Message, { - fn correct_msg_type_id(&self) -> TypeId { + fn trigger_msg_type_id(&self) -> TypeId { TypeId::of::() } } @@ -279,13 +281,13 @@ impl Recorder { self } - fn add_counter_msg(&mut self, counter_msg_setup: SingleCounterMsgSetup) { - if let Some(counter_msgs) = self.expected_future_counter_msgs_opt.as_mut() { + fn add_counter_msg(&mut self, counter_msg_setup: SingleTypeCounterMsgSetup) { + if let Some(counter_msgs) = self.counter_msgs_opt.as_mut() { counter_msgs.add_msg(counter_msg_setup) } else { let mut counter_msgs = CounterMessages::default(); counter_msgs.add_msg(counter_msg_setup); - self.expected_future_counter_msgs_opt = Some(counter_msgs) + self.counter_msgs_opt = Some(counter_msgs) } } @@ -329,8 +331,8 @@ impl Recorder { where M: ForcedMatchable + 'static, { - if let Some(counter_msgs) = self.expected_future_counter_msgs_opt.as_mut() { - counter_msgs.search_for_msg_setup(msg) + if let Some(counter_msgs) = self.counter_msgs_opt.as_mut() { + counter_msgs.search_for_msg_gear(msg) } else { None } @@ -438,11 +440,11 @@ pub struct SetUpCounterMsgs { // However, setups of the same trigger message types compose clusters. // Keep in mind these are tested over their ID method sequentially, according to the order // in which they are fed into this vector, with the other messages ignored. - setups: Vec, + setups: Vec, } impl SetUpCounterMsgs { - pub fn new(setups: Vec) -> Self { + pub fn new(setups: Vec) -> Self { Self { setups } } } @@ -824,104 +826,133 @@ mod tests { NodeToUiMessage )); let respondent_addr = respondent.start(); - let msg_1 = StartMessage {}; - let counter_msg_1 = ScanForReceivables { - response_skeleton_opt: None, - }; - // Also testing this convenient macro if it works fine - let setup_1 = setup_for_counter_msg_triggered_via_type_id!( - StartMessage, - counter_msg_1, - &respondent_addr - ); - let counter_msg_2_strayed = StartMessage {}; - let random_id = TypeId::of::(); - let id_method_2 = MsgIdentification::ByType(random_id); - let setup_2 = SingleCounterMsgSetup::new( - random_id, - id_method_2, - vec![Box::new(SendableCounterMsgWithRecipient::new( - counter_msg_2_strayed, - respondent_addr.clone().recipient(), - ))], - ); - let msg_3_unmatching = NewPublicIp { - new_ip: IpAddr::V4(Ipv4Addr::new(7, 7, 7, 7)), - }; - let msg_3_type_id = msg_3_unmatching.type_id(); - let counter_msg_3 = NodeToUiMessage { - target: MessageTarget::ClientId(4), - body: UiUnmarshalError { - message: "abc".to_string(), - bad_data: "456".to_string(), - } - .tmb(0), - }; - let id_method_3 = MsgIdentification::ByMatch { - exemplar: Box::new(NewPublicIp { new_ip: make_ip(1) }), + // Case 1 + let (trigger_message_1, cm_setup_1) = { + let trigger_msg = StartMessage {}; + let counter_msg = ScanForReceivables { + response_skeleton_opt: None, + }; + // Also testing this convenient macro if it works fine + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + StartMessage, + counter_msg, + &respondent_addr + ), + ) }; - let setup_3 = SingleCounterMsgSetup::new( - msg_3_type_id, - id_method_3, - vec![Box::new(SendableCounterMsgWithRecipient::new( - counter_msg_3, - respondent_addr.clone().recipient(), - ))], - ); - let msg_4_matching = NewPublicIp { - new_ip: IpAddr::V4(Ipv4Addr::new(4, 5, 6, 7)), + // Case two + let cm_setup_2 = { + let counter_msg_strayed = StartMessage {}; + let random_id = TypeId::of::(); + let id_method = MsgIdentification::ByType(random_id); + SingleTypeCounterMsgSetup::new( + random_id, + id_method, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg_strayed, + respondent_addr.clone().recipient(), + ))], + ) }; - let msg_4_type_id = msg_4_matching.type_id(); - let counter_msg_4 = NodeToUiMessage { - target: MessageTarget::ClientId(234), - body: UiLogBroadcast { - msg: "Good one".to_string(), - log_level: SerializableLogLevel::Error, - } - .tmb(0), + // Case three + let (trigger_msg_3_unmatching, cm_setup_3) = { + let trigger_msg = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(7, 7, 7, 7)), + }; + let type_id = trigger_msg.type_id(); + let counter_msg = NodeToUiMessage { + target: MessageTarget::ClientId(4), + body: UiUnmarshalError { + message: "abc".to_string(), + bad_data: "456".to_string(), + } + .tmb(0), + }; + let id_method = MsgIdentification::ByMatch { + exemplar: Box::new(NewPublicIp { new_ip: make_ip(1) }), + }; + ( + trigger_msg, + SingleTypeCounterMsgSetup::new( + type_id, + id_method, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg, + respondent_addr.clone().recipient(), + ))], + ), + ) }; - let id_method_4 = MsgIdentification::ByMatch { - exemplar: Box::new(msg_4_matching.clone()), + // Case four + let (trigger_msg_4_matching, cm_setup_4, counter_msg_4) = { + let trigger_msg = NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(4, 5, 6, 7)), + }; + let msg_type_id = trigger_msg.type_id(); + let counter_msg = NodeToUiMessage { + target: MessageTarget::ClientId(234), + body: UiLogBroadcast { + msg: "Good one".to_string(), + log_level: SerializableLogLevel::Error, + } + .tmb(0), + }; + let id_method = MsgIdentification::ByMatch { + exemplar: Box::new(trigger_msg.clone()), + }; + ( + trigger_msg, + SingleTypeCounterMsgSetup::new( + msg_type_id, + id_method, + vec![Box::new(SendableCounterMsgWithRecipient::new( + counter_msg.clone(), + respondent_addr.clone().recipient(), + ))], + ), + counter_msg, + ) }; - let setup_4 = SingleCounterMsgSetup::new( - msg_4_type_id, - id_method_4, - vec![Box::new(SendableCounterMsgWithRecipient::new( - counter_msg_4.clone(), - respondent_addr.clone().recipient(), - ))], - ); let system = System::new("test"); let (subject, _, subject_recording_arc) = make_recorder(); let subject_addr = subject.start(); - // Adding messages in a tangled manner + // Supplying messages deliberately in a tangled manner to express that the mechanism is + // robust enough to compensate it subject_addr .try_send(SetUpCounterMsgs { - setups: vec![setup_3, setup_1, setup_2, setup_4], + setups: vec![cm_setup_3, cm_setup_1, cm_setup_2, cm_setup_4], }) .unwrap(); - subject_addr.try_send(msg_1).unwrap(); - subject_addr.try_send(msg_3_unmatching.clone()).unwrap(); - subject_addr.try_send(msg_4_matching.clone()).unwrap(); + subject_addr.try_send(trigger_message_1).unwrap(); + subject_addr + .try_send(trigger_msg_3_unmatching.clone()) + .unwrap(); + subject_addr + .try_send(trigger_msg_4_matching.clone()) + .unwrap(); system.run(); + // Actual counter messages that flew over in this test let respondent_recording = respondent_recording_arc.lock().unwrap(); let _first_counter_msg_recorded = respondent_recording.get_record::(0); let second_counter_msg_recorded = respondent_recording.get_record::(1); assert_eq!(second_counter_msg_recorded, &counter_msg_4); assert_eq!(respondent_recording.len(), 2); + // Recorded trigger messages let subject_recording = subject_recording_arc.lock().unwrap(); - let _first_subject_recorded_msg = subject_recording.get_record::(0); - let second_subject_recorded_msg = subject_recording.get_record::(1); - assert_eq!(second_subject_recorded_msg, &msg_3_unmatching); - let third_subject_recording_msg = subject_recording.get_record::(2); - assert_eq!(third_subject_recording_msg, &msg_4_matching); + let _first_recorded_trigger_msg = subject_recording.get_record::(0); + let second_recorded_trigger_msg = subject_recording.get_record::(1); + assert_eq!(second_recorded_trigger_msg, &trigger_msg_3_unmatching); + let third_recorded_trigger_msg = subject_recording.get_record::(2); + assert_eq!(third_recorded_trigger_msg, &trigger_msg_4_matching); assert_eq!(subject_recording.len(), 3) } #[test] - fn counter_msgs_of_same_type_are_revisited_sequentially_and_triggered_by_first_matching_id_method( + fn counter_msgs_of_same_type_are_checked_sequentially_and_triggered_by_first_matching_id_method( ) { let (respondent, _, respondent_recording_arc) = make_recorder(); let respondent = respondent.system_stop_conditions(match_lazily_every_type_id!( @@ -930,71 +961,90 @@ mod tests { ConfigChangeMsg )); let respondent_addr = respondent.start(); - let msg_1 = CrashNotification { - process_id: 7777777, - exit_code: None, - stderr: Some("blah".to_string()), - }; - let counter_msg_1 = ConfigChangeMsg { - change: ConfigChange::UpdateMinHops(Hops::SixHops), - }; - let id_method_1 = MsgIdentification::ByPredicate { - predicate: Box::new(|msg_boxed| { - let msg = msg_boxed.downcast_ref::().unwrap(); - msg.process_id == 1010 - }), - }; - let setup_1 = setup_for_counter_msg_triggered_via_specific_msg_id_method!( - CrashNotification, - id_method_1, - counter_msg_1, - &respondent_addr - ); - let msg_2 = CrashNotification { - process_id: 1010, - exit_code: Some(11), - stderr: None, - }; - let counter_msg_2 = ConfigChangeMsg { - change: ConfigChange::UpdatePassword("betterPassword".to_string()), + // Case 1 + let (trigger_msg_1, cm_setup_1) = { + let trigger_msg = CrashNotification { + process_id: 7777777, + exit_code: None, + stderr: Some("blah".to_string()), + }; + let counter_msg = ConfigChangeMsg { + change: ConfigChange::UpdateMinHops(Hops::SixHops), + }; + let id_method = MsgIdentification::ByPredicate { + predicate: Box::new(|msg_boxed| { + let msg = msg_boxed.downcast_ref::().unwrap(); + msg.process_id == 1010 + }), + }; + ( + trigger_msg, + setup_for_counter_msg_triggered_via_specific_msg_id_method!( + CrashNotification, + id_method, + counter_msg, + &respondent_addr + ), + ) }; - let setup_2 = setup_for_counter_msg_triggered_via_type_id!( - CrashNotification, - counter_msg_2, - &respondent_addr - ); - let msg_3 = CrashNotification { - process_id: 9999999, - exit_code: None, - stderr: None, + // Case two + let (trigger_msg_2, cm_setup_2) = { + let trigger_msg = CrashNotification { + process_id: 1010, + exit_code: Some(11), + stderr: None, + }; + let counter_msg = ConfigChangeMsg { + change: ConfigChange::UpdatePassword("betterPassword".to_string()), + }; + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + CrashNotification, + counter_msg, + &respondent_addr + ), + ) }; - let counter_msg_3 = ConfigChangeMsg { - change: ConfigChange::UpdateWallets(WalletPair { - consuming_wallet: make_wallet("abc"), - earning_wallet: make_wallet("def"), - }), + // Case three + let (trigger_msg_3, cm_setup_3) = { + let trigger_msg = CrashNotification { + process_id: 9999999, + exit_code: None, + stderr: None, + }; + let counter_msg = ConfigChangeMsg { + change: ConfigChange::UpdateWallets(WalletPair { + consuming_wallet: make_wallet("abc"), + earning_wallet: make_wallet("def"), + }), + }; + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + CrashNotification, + counter_msg, + &respondent_addr + ), + ) }; - let setup_3 = setup_for_counter_msg_triggered_via_type_id!( - CrashNotification, - counter_msg_3, - &respondent_addr - ); let system = System::new("test"); let (subject, _, subject_recording_arc) = make_recorder(); let subject_addr = subject.start(); // Adding messages in standard order subject_addr .try_send(SetUpCounterMsgs { - setups: vec![setup_1, setup_2, setup_3], + setups: vec![cm_setup_1, cm_setup_2, cm_setup_3], }) .unwrap(); - // More tricky scenario picked on purpose - subject_addr.try_send(msg_3.clone()).unwrap(); - subject_addr.try_send(msg_2.clone()).unwrap(); - subject_addr.try_send(msg_1.clone()).unwrap(); + // Trickier scenarios picked on purpose + subject_addr.try_send(trigger_msg_3.clone()).unwrap(); + subject_addr.try_send(trigger_msg_2.clone()).unwrap(); + subject_addr.try_send(trigger_msg_1.clone()).unwrap(); system.run(); + // Actual counter messages that flew over in this test let respondent_recording = respondent_recording_arc.lock().unwrap(); let first_counter_msg_recorded = respondent_recording.get_record::(0); assert_eq!( @@ -1006,22 +1056,23 @@ mod tests { second_counter_msg_recorded.change, ConfigChange::UpdateMinHops(Hops::SixHops) ); - let second_counter_msg_recorded = respondent_recording.get_record::(2); + let third_counter_msg_recorded = respondent_recording.get_record::(2); assert_eq!( - second_counter_msg_recorded.change, + third_counter_msg_recorded.change, ConfigChange::UpdateWallets(WalletPair { consuming_wallet: make_wallet("abc"), earning_wallet: make_wallet("def") }) ); assert_eq!(respondent_recording.len(), 3); + // Recorded trigger messages let subject_recording = subject_recording_arc.lock().unwrap(); - let first_subject_recorded_msg = subject_recording.get_record::(0); - assert_eq!(first_subject_recorded_msg, &msg_3); - let second_subject_recorded_msg = subject_recording.get_record::(1); - assert_eq!(second_subject_recorded_msg, &msg_2); - let third_subject_recording_msg = subject_recording.get_record::(2); - assert_eq!(third_subject_recording_msg, &msg_1); + let first_recorded_trigger_msg = subject_recording.get_record::(0); + assert_eq!(first_recorded_trigger_msg, &trigger_msg_3); + let second_recorded_trigger_msg = subject_recording.get_record::(1); + assert_eq!(second_recorded_trigger_msg, &trigger_msg_2); + let third_recorded_trigger_msg = subject_recording.get_record::(2); + assert_eq!(third_recorded_trigger_msg, &trigger_msg_1); assert_eq!(subject_recording.len(), 3) } } diff --git a/node/src/test_utils/recorder_counter_msgs.rs b/node/src/test_utils/recorder_counter_msgs.rs index 9a8997724..252431224 100644 --- a/node/src/test_utils/recorder_counter_msgs.rs +++ b/node/src/test_utils/recorder_counter_msgs.rs @@ -46,59 +46,65 @@ where } } -pub struct SingleCounterMsgSetup { +pub struct SingleTypeCounterMsgSetup { // Leave them private - trigger_msg_type_id: TypeId, - condition: MsgIdentification, - // Multiple messages sent off on reaction are allowed - // (Imagine a message handler whose execution goes about more than just one msg dispatch) + trigger_msg_type_id: TriggerMsgTypeId, + trigger_msg_id_method: MsgIdentification, + // Responding by multiple outbound messages to a single incoming (trigger) message is supported. + // (Imitates a message handler whose execution implies a couple of message dispatches) msg_gears: Vec>, } -impl SingleCounterMsgSetup { +impl SingleTypeCounterMsgSetup { pub fn new( - trigger_msg_type_id: TypeId, + trigger_msg_type_id: TriggerMsgTypeId, trigger_msg_id_method: MsgIdentification, - counter_messages: Vec>, + msg_gears: Vec>, ) -> Self { Self { trigger_msg_type_id, - condition: trigger_msg_id_method, - msg_gears: counter_messages, + trigger_msg_id_method, + msg_gears, } } } +pub type TriggerMsgTypeId = TypeId; + #[derive(Default)] pub struct CounterMessages { - msgs: HashMap>, + msgs: HashMap>, } impl CounterMessages { - pub fn search_for_msg_setup(&mut self, msg: &Msg) -> Option>> + pub fn search_for_msg_gear( + &mut self, + trigger_msg: &Msg, + ) -> Option>> where Msg: ForcedMatchable + 'static, { - let type_id = msg.correct_msg_type_id(); + let type_id = trigger_msg.trigger_msg_type_id(); if let Some(msgs_vec) = self.msgs.get_mut(&type_id) { msgs_vec .iter_mut() - .position(|cm_setup| cm_setup.condition.resolve_condition(msg)) - .map(|idx| { - let matching_counter_msg = msgs_vec.remove(idx); - matching_counter_msg.msg_gears + .position(|cm_setup| { + cm_setup + .trigger_msg_id_method + .resolve_condition(trigger_msg) }) + .map(|idx| msgs_vec.remove(idx).msg_gears) } else { None } } - pub fn add_msg(&mut self, counter_msg_setup: SingleCounterMsgSetup) { + pub fn add_msg(&mut self, counter_msg_setup: SingleTypeCounterMsgSetup) { let type_id = counter_msg_setup.trigger_msg_type_id; match self.msgs.entry(type_id) { - Entry::Occupied(mut existing) => existing.get_mut().push(counter_msg_setup), - Entry::Vacant(vacant) => { - vacant.insert(vec![counter_msg_setup]); + Entry::Occupied(mut existing_vec) => existing_vec.get_mut().push(counter_msg_setup), + Entry::Vacant(vacancy) => { + vacancy.insert(vec![counter_msg_setup]); } } } @@ -133,7 +139,7 @@ macro_rules! setup_for_counter_msg_triggered_via_specific_msg_id_method{ )),+ ]; - SingleCounterMsgSetup::new( + SingleTypeCounterMsgSetup::new( TypeId::of::<$trigger_msg_type>(), $msg_id_method, msg_gears diff --git a/node/src/test_utils/recorder_stop_conditions.rs b/node/src/test_utils/recorder_stop_conditions.rs index 137a4a17b..6440b0289 100644 --- a/node/src/test_utils/recorder_stop_conditions.rs +++ b/node/src/test_utils/recorder_stop_conditions.rs @@ -121,8 +121,8 @@ impl MsgIdentification { } fn matches_by_type>(msg: &Msg, expected_type_id: TypeId) -> bool { - let correct_msg_type_id = msg.correct_msg_type_id(); - correct_msg_type_id == expected_type_id + let trigger_msg_type_id = msg.trigger_msg_type_id(); + trigger_msg_type_id == expected_type_id } fn matches_completely + 'static + Send>( @@ -144,7 +144,7 @@ impl MsgIdentification { } pub trait ForcedMatchable: PartialEq + Send { - fn correct_msg_type_id(&self) -> TypeId; + fn trigger_msg_type_id(&self) -> TypeId; } pub struct PretendedMatchableWrapper(pub M); @@ -154,7 +154,7 @@ where OuterM: PartialEq, InnerM: Send, { - fn correct_msg_type_id(&self) -> TypeId { + fn trigger_msg_type_id(&self) -> TypeId { TypeId::of::() } } @@ -173,7 +173,13 @@ impl PartialEq for PretendedMatchableWrapper { #[macro_export] macro_rules! match_lazily_every_type_id{ ($($single_message: ident),+) => { - StopConditions::AllLazily(vec![$(MsgIdentification::ByType(TypeId::of::<$single_message>())),+]) + StopConditions::AllLazily(vec![ + $( + crate::test_utils::recorder_stop_conditions::MsgIdentification::ByType( + TypeId::of::<$single_message>() + ) + ),+ + ]) } } From 1edcf82060dba25c2f9c11723640cd8e6b0aa3ba Mon Sep 17 00:00:00 2001 From: Bert Date: Tue, 3 Jun 2025 12:18:00 +0200 Subject: [PATCH 34/49] GH-602: addressing review continues... --- node/src/accountant/mod.rs | 165 ++++++++++-------- node/src/accountant/scanners/mod.rs | 94 +++++----- .../accountant/scanners/scan_schedulers.rs | 156 ++++++++--------- .../test_utils/recorder_stop_conditions.rs | 84 ++++----- 4 files changed, 260 insertions(+), 239 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 370ed6080..6ae342391 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -76,7 +76,7 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; -use crate::accountant::scanners::scan_schedulers::{HintableScanner, SchedulingAfterHaltedScan, ScanSchedulers}; +use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, SchedulingAfterHaltedScan, ScanSchedulers}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; @@ -221,7 +221,7 @@ impl Handler for Accountant { fn handle(&mut self, msg: ScanForPendingPayables, ctx: &mut Self::Context) -> Self::Result { // By now we know this is an automatic scan process. The scan may be or may not be - // rescheduled. It depends on the findings. Any failed transactions will lead to the launch + // rescheduled. It depends on the findings. Any failed transaction will lead to the launch // of the RetryPayableScanner, which finishes, and the PendingPayablesScanner is scheduled // to run again. However, not from here. let response_skeleton_opt = msg.response_skeleton_opt; @@ -230,7 +230,7 @@ impl Handler for Accountant { { self.scan_schedulers .payable - .schedule_for_new_payable(ctx, &self.logger); + .schedule_new_payable_scan(ctx, &self.logger); } } } @@ -239,8 +239,8 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForNewPayables, ctx: &mut Self::Context) -> Self::Result { - // We know this must be a scheduled scan, but are yet clueless if where it's going to be - // rescheduled. If no payables qualify for a payment, we do it here right away. If some + // We know this must be a scheduled scan, but are yet clueless where it's going to be + // rescheduled. If no payable qualifies for a payment, we do it here right away. If some // transactions made it out, the next scheduling of this scanner is going to be decided by // the PendingPayableScanner whose job is to evaluate if it has seen every pending payable // complete. That's the moment when another run of the NewPayableScanner makes sense again. @@ -250,7 +250,7 @@ impl Handler for Accountant { { self.scan_schedulers .payable - .schedule_for_new_payable(ctx, &self.logger) + .schedule_new_payable_scan(ctx, &self.logger) } } } @@ -260,7 +260,7 @@ impl Handler for Accountant { fn handle(&mut self, msg: ScanForRetryPayables, _ctx: &mut Self::Context) -> Self::Result { // RetryPayableScanner is scheduled only when the PendingPayableScanner finishes discovering - // that there have been some failed pending payables. No place for it here. + // that there have been some failed payables. No place for that here. let response_skeleton = msg.response_skeleton_opt; let _ = self.handle_request_of_scan_for_retry_payable(response_skeleton); } @@ -270,8 +270,8 @@ impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: ScanForReceivables, ctx: &mut Self::Context) -> Self::Result { - // By now we know it is an automatic scan, which, for this particular scanner, is - // rescheduled pretty regularly, right here, no matter what its outcome is. + // By now we know it is an automatic scan. The ReceivableScanner is independent of other + // scanners and rescheduled regularly, just here. self.handle_request_of_scan_for_receivable(msg.response_skeleton_opt); self.scan_schedulers.receivable.schedule(ctx, &self.logger); } @@ -291,17 +291,17 @@ impl Handler for Accountant { .try_send(node_to_ui_msg) .expect("UIGateway is dead"); // Externally triggered scan should never be allowed to spark a procedure that - // would bring around payables with fresh nonces. The job's done. + // would bring over payables with fresh nonces. The job's done. } else { self.scan_schedulers .payable - .schedule_for_new_payable(ctx, &self.logger) + .schedule_new_payable_scan(ctx, &self.logger) } } PendingPayableScanResult::PaymentRetryRequired => self .scan_schedulers .payable - .schedule_for_retry_payable(ctx, response_skeleton_opt, &self.logger), + .schedule_retry_payable_scan(ctx, response_skeleton_opt, &self.logger), }; } } @@ -333,7 +333,7 @@ impl Handler for Accountant { OperationOutcome::Failure => self .scan_schedulers .payable - .schedule_for_new_payable(ctx, &self.logger), + .schedule_new_payable_scan(ctx, &self.logger), }, Some(node_to_ui_msg) => { self.ui_message_sub_opt @@ -915,8 +915,8 @@ impl Accountant { .expect("BlockchainBridge is dead"); SchedulingAfterHaltedScan::DoNotSchedule } - Err(e) => self.handle_start_scan_error_for_scanner_with_irregular_scheduling( - HintableScanner::NewPayables, + Err(e) => self.handle_start_scan_error_and_prevent_scan_stall_point( + PayableSequenceScanner::NewPayables, e, response_skeleton_opt, ), @@ -947,8 +947,8 @@ impl Accountant { .expect("BlockchainBridge is dead"); SchedulingAfterHaltedScan::DoNotSchedule } - Err(e) => self.handle_start_scan_error_for_scanner_with_irregular_scheduling( - HintableScanner::RetryPayables, + Err(e) => self.handle_start_scan_error_and_prevent_scan_stall_point( + PayableSequenceScanner::RetryPayables, e, response_skeleton_opt, ), @@ -982,8 +982,8 @@ impl Accountant { } Err(e) => { let initial_pending_payable_scan = self.scanners.initial_pending_payable_scan(); - self.handle_start_scan_error_for_scanner_with_irregular_scheduling( - HintableScanner::PendingPayables { + self.handle_start_scan_error_and_prevent_scan_stall_point( + PayableSequenceScanner::PendingPayables { initial_pending_payable_scan, }, e, @@ -999,19 +999,15 @@ impl Accountant { hint } - fn handle_start_scan_error_for_scanner_with_irregular_scheduling( + fn handle_start_scan_error_and_prevent_scan_stall_point( &self, - hintable_scanner: HintableScanner, + scanner: PayableSequenceScanner, e: StartScanError, response_skeleton_opt: Option, ) -> SchedulingAfterHaltedScan { let is_externally_triggered = response_skeleton_opt.is_some(); - e.log_error( - &self.logger, - hintable_scanner.into(), - is_externally_triggered, - ); + e.log_error(&self.logger, scanner.into(), is_externally_triggered); response_skeleton_opt.map(|skeleton| { self.ui_message_sub_opt @@ -1026,7 +1022,7 @@ impl Accountant { self.scan_schedulers .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error(hintable_scanner, &e, is_externally_triggered) + .resolve_rescheduling_for_given_error(scanner, &e, is_externally_triggered) } fn handle_request_of_scan_for_receivable( @@ -1377,7 +1373,7 @@ mod tests { let financial_statistics = result.financial_statistics().clone(); let default_scan_intervals = ScanIntervals::default(); assert_eq!( - result.scan_schedulers.payable.nominal_interval, + result.scan_schedulers.payable.new_payable_interval, default_scan_intervals.payable_scan_interval ); assert_eq!( @@ -1389,6 +1385,10 @@ mod tests { default_scan_intervals.receivable_scan_interval, ); assert_eq!(result.scan_schedulers.automatic_scans_enabled, true); + assert_eq!( + result.scanners.aware_of_unresolved_pending_payables(), + false + ); assert_eq!(result.consuming_wallet_opt, None); assert_eq!(result.earning_wallet, *DEFAULT_EARNING_WALLET); result @@ -1492,7 +1492,7 @@ mod tests { // Making sure we would get a panic if another scan was scheduled subject.scan_schedulers.payable.new_payable_notify_later = Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); - subject.scan_schedulers.payable.nominal_interval = Duration::from_secs(100); + subject.scan_schedulers.payable.new_payable_interval = Duration::from_secs(100); let subject_addr = subject.start(); let system = System::new("test"); let ui_message = NodeFromUiMessage { @@ -1819,7 +1819,7 @@ mod tests { // Making sure we would get a panic if another scan was scheduled subject.scan_schedulers.payable.new_payable_notify_later = Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); - subject.scan_schedulers.payable.nominal_interval = Duration::from_secs(100); + subject.scan_schedulers.payable.new_payable_interval = Duration::from_secs(100); let subject_addr = subject.start(); let ui_message = NodeFromUiMessage { client_id: 1234, @@ -2773,15 +2773,22 @@ mod tests { .consuming_wallet(make_wallet("consuming")) .pending_payable_daos(vec![ForPendingPayableScanner(pending_payable_dao)]) .build(); - subject.request_transaction_receipts_sub_opt = Some(make_recorder().0.start().recipient()); + let system = System::new("test"); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge_addr = blockchain_bridge.start(); + subject.request_transaction_receipts_sub_opt = Some(blockchain_bridge_addr.recipient()); let flag_before = subject.scanners.initial_pending_payable_scan(); let hint = subject.handle_request_of_scan_for_pending_payable(None); + System::current().stop(); + system.run(); let flag_after = subject.scanners.initial_pending_payable_scan(); assert_eq!(hint, SchedulingAfterHaltedScan::DoNotSchedule); assert_eq!(flag_before, true); assert_eq!(flag_after, false); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + let _recorded_msg = blockchain_bridge_recording.get_record::(0); } #[test] @@ -2856,12 +2863,6 @@ mod tests { .bootstrapper_config(config) .logger(Logger::new(test_name)) .build(); - // Important. Preventing the possibly endless sequence of - // PendingPayableScanner -> NewPayableScanner -> NewPayableScanner... - subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default()); - subject - .scanners - .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); subject .scanners .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Mock( @@ -2877,7 +2878,11 @@ mod tests { let peer_actors = peer_actors_builder().build(); send_bind_message!(subject_subs, peer_actors); - send_start_message!(subject_subs); + subject_addr + .try_send(ScanForReceivables { + response_skeleton_opt: None, + }) + .unwrap(); let time_before = SystemTime::now(); system.run(); @@ -3234,7 +3239,8 @@ mod tests { balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei - 1), last_paid_timestamp: from_unix_timestamp( now - checked_conversion::( - payment_thresholds.threshold_interval_sec + 10, + payment_thresholds.maturity_threshold_sec + + payment_thresholds.threshold_interval_sec, ), ), pending_payable_opt: None, @@ -3244,20 +3250,20 @@ mod tests { wallet: make_wallet("wallet1"), balance_wei: gwei_to_wei(payment_thresholds.debt_threshold_gwei + 1), last_paid_timestamp: from_unix_timestamp( - now - checked_conversion::( - payment_thresholds.maturity_threshold_sec - 10, - ), + now - checked_conversion::(payment_thresholds.maturity_threshold_sec) + + 1, ), pending_payable_opt: None, }, // above minimum balance, to the right of minimum time (not in buffer zone, below the curve) PayableAccount { wallet: make_wallet("wallet2"), - balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 55), + balance_wei: gwei_to_wei::( + payment_thresholds.permanent_debt_allowed_gwei, + ) + 1, last_paid_timestamp: from_unix_timestamp( - now - checked_conversion::( - payment_thresholds.maturity_threshold_sec + 15, - ), + now - checked_conversion::(payment_thresholds.threshold_interval_sec) + + 1, ), pending_payable_opt: None, }, @@ -3270,24 +3276,36 @@ mod tests { "scan_for_new_payables_does_not_trigger_payment_for_balances_below_the_curve", ); let blockchain_bridge_addr: Addr = blockchain_bridge.start(); - let outbound_payments_instructions_sub = - blockchain_bridge_addr.recipient::(); let mut subject = AccountantBuilder::default() .bootstrapper_config(config) - .payable_daos(vec![ForPayableScanner(payable_dao)]) + .consuming_wallet(consuming_wallet.clone()) .build(); - subject.outbound_payments_instructions_sub_opt = Some(outbound_payments_instructions_sub); + let payable_scanner = PayableScannerBuilder::new() + .payment_thresholds(payment_thresholds) + .payable_dao(payable_dao) + .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Real( + payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Null)); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); + bind_ui_gateway_unasserted(&mut subject); - let _result = subject.scanners.start_new_payable_scan_guarded( - &consuming_wallet, - SystemTime::now(), - None, - &subject.logger, - true, - ); + let result = subject.handle_request_of_scan_for_new_payable(None); System::current().stop(); system.run(); + assert_eq!( + result, + SchedulingAfterHaltedScan::Schedule(ScanType::Payables) + ); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recordings.len(), 0); } @@ -3348,15 +3366,10 @@ mod tests { subject .scanners .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); - subject.scan_schedulers.automatic_scans_enabled = false; subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.recipient()); bind_ui_gateway_unasserted(&mut subject); - let response_skeleton_opt = Some(ResponseSkeleton { - client_id: 31, - context_id: 24, - }); - subject.handle_request_of_scan_for_new_payable(response_skeleton_opt); + subject.handle_request_of_scan_for_new_payable(None); System::current().stop(); system.run(); @@ -3367,7 +3380,7 @@ mod tests { &QualifiedPayablesMessage { qualified_payables, consuming_wallet, - response_skeleton_opt, + response_skeleton_opt: None, } ); } @@ -4214,10 +4227,10 @@ mod tests { } #[test] - fn zero_sent_payables_so_the_payable_scan_is_rescheduled_without_scanning_for_pending_payables() - { + fn no_payables_left_the_node_so_payable_scan_is_rescheduled_as_pending_payable_scan_was_omitted( + ) { init_test_logging(); - let test_name = "zero_sent_payables_so_the_payable_scan_is_rescheduled_without_scanning_for_pending_payables"; + let test_name = "no_payables_left_the_node_so_payable_scan_is_rescheduled_as_pending_payable_scan_was_omitted"; let finish_scan_params_arc = Arc::new(Mutex::new(vec![])); let payable_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let system = System::new(test_name); @@ -4246,6 +4259,8 @@ mod tests { subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default().notify_later_params(&payable_notify_later_params_arc), ); + subject.scan_schedulers.pending_payable.handle = + Box::new(NotifyLaterHandleMock::default().panic_on_schedule_attempt()); let sent_payable = SentPayables { payment_procedure_result: Err(PayableTransactionError::Sending { msg: "booga".to_string(), @@ -4345,7 +4360,7 @@ mod tests { let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() .compute_interval_params(&compute_interval_params_arc) .compute_interval_result(Some(expected_computed_interval)); - subject.scan_schedulers.payable.nominal_interval = nominal_interval; + subject.scan_schedulers.payable.new_payable_interval = nominal_interval; subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); subject .scan_schedulers @@ -4421,7 +4436,7 @@ mod tests { let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() .compute_interval_params(&compute_interval_params_arc) .compute_interval_result(None); - subject.scan_schedulers.payable.nominal_interval = nominal_interval; + subject.scan_schedulers.payable.new_payable_interval = nominal_interval; subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); subject .scan_schedulers @@ -4487,8 +4502,8 @@ mod tests { let last_new_payable_scan_timestamp = SystemTime::now() .checked_sub(Duration::from_millis(3500)) .unwrap(); - let nominal_interval = Duration::from_secs(6); - subject.scan_schedulers.payable.nominal_interval = nominal_interval; + let new_payable_interval = Duration::from_secs(6); + subject.scan_schedulers.payable.new_payable_interval = new_payable_interval; subject .scan_schedulers .payable @@ -4521,10 +4536,14 @@ mod tests { let (_, actual_interval) = new_payable_notify_later[0]; let interval_computer = NewPayableScanDynIntervalComputerReal::default(); let left_side_bound = interval_computer - .compute_interval(before, last_new_payable_scan_timestamp, nominal_interval) + .compute_interval( + before, + last_new_payable_scan_timestamp, + new_payable_interval, + ) .unwrap(); let right_side_bound = interval_computer - .compute_interval(after, last_new_payable_scan_timestamp, nominal_interval) + .compute_interval(after, last_new_payable_scan_timestamp, new_payable_interval) .unwrap(); assert!( left_side_bound >= actual_interval && actual_interval >= right_side_bound, diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 3ab89a3ee..6e18825f1 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -129,7 +129,6 @@ impl Scanners { MTError::AutomaticScanConflict, )); } - if let Some(started_at) = self.payable.scan_started_at() { return Err(StartScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Payables, @@ -192,12 +191,12 @@ impl Scanners { self.payable.scan_started_at(), ) { (Some(pp_timestamp), Some(p_timestamp)) => - // If you're wondering, then yes, this condition should be the sacre truth between + // If you're wondering, then yes, this condition should be the sacred truth between // PendingPayableScanner and NewPayableScanner. { unreachable!( - "Both payable scanners should never be allowed to run in parallel. Scan for \ - pending payables started at: {}, scan for payables started at: {}", + "Any payable-related scanners should never be allowed to run in parallel. \ + Scan for pending payables started at: {}, scan for payables started at: {}", StartScanError::timestamp_as_string(pp_timestamp), StartScanError::timestamp_as_string(p_timestamp) ) @@ -229,7 +228,6 @@ impl Scanners { automatic_scans_enabled: bool, ) -> Result { let triggered_manually = response_skeleton_opt.is_some(); - if triggered_manually && automatic_scans_enabled { return Err(StartScanError::ManualTriggerError( MTError::AutomaticScanConflict, @@ -241,6 +239,7 @@ impl Scanners { started_at, }); } + self.receivable .start_scan(wallet, timestamp, response_skeleton_opt, logger) } @@ -308,6 +307,10 @@ impl Scanners { self.initial_pending_payable_scan = false } + // This is a helper function reducing a boilerplate of complex trait resolving where + // the compiler requires to specify which trigger message distinguish the scan to run. + // The payable scanner offers two modes through doubled implementations of StartableScanner + // which uses the trigger message type as the only distinction between them. fn start_correct_payable_scanner<'a, TriggerMessage>( scanner: &'a mut (dyn MultistageDualPayableScanner + 'a), wallet: &Wallet, @@ -332,25 +335,28 @@ impl Scanners { automatic_scans_enabled: bool, ) -> Result<(), StartScanError> { if triggered_manually && automatic_scans_enabled { - Err(StartScanError::ManualTriggerError( + return Err(StartScanError::ManualTriggerError( MTError::AutomaticScanConflict, - )) - } else if self.initial_pending_payable_scan { - Ok(()) - } else if triggered_manually && !self.aware_of_unresolved_pending_payable { - Err(StartScanError::ManualTriggerError( + )); + } + if self.initial_pending_payable_scan { + return Ok(()); + } + if triggered_manually && !self.aware_of_unresolved_pending_payable { + return Err(StartScanError::ManualTriggerError( MTError::UnnecessaryRequest { hint_opt: Some("Run the Payable scanner first.".to_string()), }, - )) - } else if !self.aware_of_unresolved_pending_payable { + )); + } + if !self.aware_of_unresolved_pending_payable { unreachable!( "Automatic pending payable scan should never start if there are no pending \ payables to process." ) - } else { - Ok(()) } + + Ok(()) } } @@ -509,7 +515,7 @@ impl StartableScanner for Payabl _response_skeleton_opt: Option, _logger: &Logger, ) -> Result { - todo!() + todo!("Complete me under GH-605") } } @@ -893,21 +899,14 @@ impl Scanner for PendingPay message: ReportTransactionReceipts, logger: &Logger, ) -> PendingPayableScanResult { - let construct_msg_scan_ended_to_ui = move || { - message - .response_skeleton_opt - .map(|response_skeleton| NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) - }; + let response_skeleton_opt = message.response_skeleton_opt; let requires_payment_retry = match message.fingerprints_with_receipts.is_empty() { true => { - debug!(logger, "No transaction receipts found."); + warning!(logger, "No transaction receipts found."); todo!( - "requires payment retry...it must be processed across the new methods on \ - the SentPaybleDAO" + "This requires the payment retry. It must be processed by using some of the new \ + methods on the SentPaybleDAO. After GH-631 is done." ); } false => { @@ -929,7 +928,11 @@ impl Scanner for PendingPay if requires_payment_retry { PendingPayableScanResult::PaymentRetryRequired } else { - PendingPayableScanResult::NoPendingPayablesLeft(construct_msg_scan_ended_to_ui()) + let ui_msg_opt = response_skeleton_opt.map(|response_skeleton| NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }); + PendingPayableScanResult::NoPendingPayablesLeft(ui_msg_opt) } } @@ -1675,7 +1678,7 @@ mod tests { use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport, PendingPayableScanResult}; use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, MTError}; use crate::accountant::test_utils::{make_custom_payment_thresholds, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder}; - use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForNewPayables, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; + use crate::accountant::{gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ScanError, ScanForRetryPayables, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC}; use crate::blockchain::blockchain_bridge::{BlockMarker, PendingPayableFingerprint, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::blockchain::blockchain_interface::data_structures::{ @@ -1762,6 +1765,10 @@ mod tests { } } + pub fn aware_of_unresolved_pending_payables(&self) -> bool { + self.aware_of_unresolved_pending_payable + } + pub fn set_aware_of_unresolved_pending_payables(&mut self, value: bool) { self.aware_of_unresolved_pending_payable = value } @@ -1981,19 +1988,22 @@ mod tests { make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); let payable_dao = PayableDaoMock::new().non_pending_payables_result(unqualified_payable_accounts); - let mut subject = PayableScannerBuilder::new() - .payable_dao(payable_dao) - .build(); + let mut subject = make_dull_subject(); + subject.payable = Box::new( + PayableScannerBuilder::new() + .payable_dao(payable_dao) + .build(), + ); - let result = Scanners::start_correct_payable_scanner::( - &mut subject, + let result = subject.start_new_payable_scan_guarded( &consuming_wallet, - now, + SystemTime::now(), None, &Logger::new("test"), + true, ); - let is_scan_running = subject.scan_started_at().is_some(); + let is_scan_running = subject.scan_started_at(ScanType::Payables).is_some(); assert_eq!(is_scan_running, false); assert_eq!(result, Err(StartScanError::NothingToProcess)); } @@ -3211,8 +3221,9 @@ mod tests { .unwrap_err(); let panic_msg = caught_panic.downcast_ref::().unwrap(); - let expected_msg_fragment_1 = "internal error: entered unreachable code: Both payable \ - scanners should never be allowed to run in parallel. Scan for pending payables started at: "; + let expected_msg_fragment_1 = "internal error: entered unreachable code: Any payable-\ + related scanners should never be allowed to run in parallel. Scan for pending payables \ + started at: "; assert!( panic_msg.contains(expected_msg_fragment_1), "This fragment '{}' wasn't found in \ @@ -3909,7 +3920,7 @@ mod tests { assert_eq!(is_scan_running, false); let tlh = TestLogHandler::new(); tlh.exists_log_containing(&format!( - "ERROR: {test_name}: No transaction receipts found." + "WARN: {test_name}: No transaction receipts found." )); tlh.exists_log_matching(&format!( "INFO: {test_name}: The PendingPayables scan ended in \\d+ms." @@ -4513,13 +4524,14 @@ mod tests { fn log_error_works_fine_for_automatic_scanning() { init_test_logging(); let test_name = "log_error_works_fine_for_automatic_scanning"; + let now = SystemTime::now(); let input = vec![ ( StartScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Payables, - started_at: SystemTime::now() + started_at: now }, - format!("DEBUG: {test_name}: Payables scan was already initiated at" /*TODO suboptimal */) + format!("DEBUG: {test_name}: Payables scan was already initiated at {}", StartScanError::timestamp_as_string(now)) ), ( StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index dcf83324a..727bd75a7 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -50,18 +50,18 @@ pub enum SchedulingAfterHaltedScan { } #[derive(Debug, PartialEq, Clone, Copy)] -pub enum HintableScanner { +pub enum PayableSequenceScanner { NewPayables, RetryPayables, PendingPayables { initial_pending_payable_scan: bool }, } -impl From for ScanType { - fn from(hintable_scanner: HintableScanner) -> Self { - match hintable_scanner { - HintableScanner::NewPayables => ScanType::Payables, - HintableScanner::RetryPayables => ScanType::Payables, - HintableScanner::PendingPayables { .. } => ScanType::PendingPayables, +impl From for ScanType { + fn from(scanner: PayableSequenceScanner) -> Self { + match scanner { + PayableSequenceScanner::NewPayables => ScanType::Payables, + PayableSequenceScanner::RetryPayables => ScanType::Payables, + PayableSequenceScanner::PendingPayables { .. } => ScanType::PendingPayables, } } } @@ -70,32 +70,32 @@ pub struct PayableScanScheduler { pub new_payable_notify_later: Box>, pub dyn_interval_computer: Box, pub inner: Arc>, - pub nominal_interval: Duration, + pub new_payable_interval: Duration, pub new_payable_notify: Box>, pub retry_payable_notify: Box>, } impl PayableScanScheduler { - fn new(nominal_interval: Duration) -> Self { + fn new(new_payable_interval: Duration) -> Self { Self { new_payable_notify_later: Box::new(NotifyLaterHandleReal::default()), dyn_interval_computer: Box::new(NewPayableScanDynIntervalComputerReal::default()), inner: Arc::new(Mutex::new(PayableScanSchedulerInner::default())), - nominal_interval, + new_payable_interval, new_payable_notify: Box::new(NotifyHandleReal::default()), retry_payable_notify: Box::new(NotifyHandleReal::default()), } } - pub fn schedule_for_new_payable(&self, ctx: &mut Context, logger: &Logger) { + pub fn schedule_new_payable_scan(&self, ctx: &mut Context, logger: &Logger) { let inner = self.inner.lock().expect("couldn't acquire inner"); - let last_new_payable_timestamp = inner.last_new_payable_scan_timestamp; - let nominal_interval = self.nominal_interval; + let last_new_payable_scan_timestamp = inner.last_new_payable_scan_timestamp; + let new_payable_interval = self.new_payable_interval; let now = SystemTime::now(); if let Some(interval) = self.dyn_interval_computer.compute_interval( now, - last_new_payable_timestamp, - nominal_interval, + last_new_payable_scan_timestamp, + new_payable_interval, ) { debug!( logger, @@ -125,7 +125,7 @@ impl PayableScanScheduler { // Can be triggered by a command, whereas the finished pending payable scanner is followed up // by this scheduled message that can bear the response skeleton. This message is inserted into // the Accountant's mailbox with no delay - pub fn schedule_for_retry_payable( + pub fn schedule_retry_payable_scan( &self, ctx: &mut Context, response_skeleton_opt: Option, @@ -230,7 +230,7 @@ where pub trait RescheduleScanOnErrorResolver { fn resolve_rescheduling_for_given_error( &self, - hintable_scanner: HintableScanner, + scanner: PayableSequenceScanner, error: &StartScanError, is_externally_triggered: bool, ) -> SchedulingAfterHaltedScan; @@ -242,18 +242,18 @@ pub struct RescheduleScanOnErrorResolverReal {} impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverReal { fn resolve_rescheduling_for_given_error( &self, - hintable_scanner: HintableScanner, + scanner: PayableSequenceScanner, error: &StartScanError, is_externally_triggered: bool, ) -> SchedulingAfterHaltedScan { - match hintable_scanner { - HintableScanner::NewPayables => { + match scanner { + PayableSequenceScanner::NewPayables => { Self::resolve_new_payables(error, is_externally_triggered) } - HintableScanner::RetryPayables => { + PayableSequenceScanner::RetryPayables => { Self::resolve_retry_payables(error, is_externally_triggered) } - HintableScanner::PendingPayables { + PayableSequenceScanner::PendingPayables { initial_pending_payable_scan, } => Self::resolve_pending_payables( error, @@ -328,8 +328,8 @@ impl RescheduleScanOnErrorResolverReal { #[cfg(test)] mod tests { use crate::accountant::scanners::scan_schedulers::{ - HintableScanner, NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, - ScanSchedulers, SchedulingAfterHaltedScan, + NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, + PayableSequenceScanner, ScanSchedulers, SchedulingAfterHaltedScan, }; use crate::accountant::scanners::{MTError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; @@ -351,7 +351,7 @@ mod tests { let schedulers = ScanSchedulers::new(scan_intervals, automatic_scans_enabled); assert_eq!( - schedulers.payable.nominal_interval, + schedulers.payable.new_payable_interval, scan_intervals.payable_scan_interval ); let payable_scheduler_inner = schedulers.payable.inner.lock().unwrap(); @@ -367,7 +367,7 @@ mod tests { schedulers.receivable.interval, scan_intervals.receivable_scan_interval ); - assert_eq!(schedulers.automatic_scans_enabled, true) + assert_eq!(schedulers.automatic_scans_enabled, automatic_scans_enabled) } #[test] @@ -492,22 +492,22 @@ mod tests { let mut check_vec = candidates .iter() .fold(vec![],|mut acc, current|{ - acc.push(AllStartScanErrorsAdjustable::number_variant(current)); + acc.push(ListOfStartScanErrors::number_variant(current)); acc }); // Making sure we didn't count in one variant multiple times check_vec.dedup(); - assert_eq!(check_vec.len(), StartScanError::VARIANT_COUNT, "Check on variant \ + assert_eq!(check_vec.len(), StartScanError::VARIANT_COUNT, "The check on variant \ exhaustiveness failed."); candidates }; } - struct AllStartScanErrorsAdjustable<'a> { + struct ListOfStartScanErrors<'a> { errors: Vec<&'a StartScanError>, } - impl<'a> Default for AllStartScanErrorsAdjustable<'a> { + impl<'a> Default for ListOfStartScanErrors<'a> { fn default() -> Self { Self { errors: ALL_START_SCAN_ERRORS.iter().collect_vec(), @@ -515,31 +515,17 @@ mod tests { } } - impl<'a> AllStartScanErrorsAdjustable<'a> { + impl<'a> ListOfStartScanErrors<'a> { fn eliminate_already_tested_variants( mut self, errors_to_eliminate: Vec, ) -> Self { - let original_errors_tuples = self - .errors - .iter() - .map(|err| (Self::number_variant(*err), err)) - .collect_vec(); - let errors_to_eliminate_num_rep = errors_to_eliminate + let error_variants_to_remove: Vec<_> = errors_to_eliminate .iter() .map(Self::number_variant) - .collect_vec(); - let adjusted = errors_to_eliminate_num_rep - .into_iter() - .fold(original_errors_tuples, |acc, current| { - acc.into_iter() - .filter(|(num, _)| num != ¤t) - .collect_vec() - }) - .into_iter() - .map(|(_, err)| *err) - .collect_vec(); - self.errors = adjusted; + .collect(); + self.errors + .retain(|err| !error_variants_to_remove.contains(&Self::number_variant(*err))); self } @@ -560,13 +546,13 @@ mod tests { test_what_if_externally_triggered( &subject, - HintableScanner::PendingPayables { + PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: false, }, ); test_what_if_externally_triggered( &subject, - HintableScanner::PendingPayables { + PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: true, }, ); @@ -574,7 +560,7 @@ mod tests { fn test_what_if_externally_triggered( subject: &ScanSchedulers, - hintable_scanner: HintableScanner, + scanner: PayableSequenceScanner, ) { ALL_START_SCAN_ERRORS .iter() @@ -582,7 +568,7 @@ mod tests { .for_each(|(idx, error)| { let result = subject .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error(hintable_scanner, error, true); + .resolve_rescheduling_for_given_error(scanner, error, true); assert_eq!( result, @@ -590,7 +576,7 @@ mod tests { "We expected DoNotSchedule but got {:?} at idx {} for {:?}", result, idx, - hintable_scanner + scanner ); }) } @@ -603,7 +589,7 @@ mod tests { let result = subject .reschedule_on_error_resolver .resolve_rescheduling_for_given_error( - HintableScanner::PendingPayables { + PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: true, }, &StartScanError::NothingToProcess, @@ -631,7 +617,7 @@ mod tests { let _ = subject .reschedule_on_error_resolver .resolve_rescheduling_for_given_error( - HintableScanner::PendingPayables { + PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: false, }, &StartScanError::NothingToProcess, @@ -643,12 +629,12 @@ mod tests { fn resolve_error_for_pending_payables_if_no_consuming_wallet_found() { fn test_no_consuming_wallet_found( subject: &ScanSchedulers, - hintable_scanner: HintableScanner, + scanner: PayableSequenceScanner, ) { let result = subject .reschedule_on_error_resolver .resolve_rescheduling_for_given_error( - hintable_scanner, + scanner, &StartScanError::NoConsumingWalletFound, false, ); @@ -658,7 +644,7 @@ mod tests { SchedulingAfterHaltedScan::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got {:?} for {:?}", result, - hintable_scanner + scanner ); } @@ -666,13 +652,13 @@ mod tests { test_no_consuming_wallet_found( &subject, - HintableScanner::PendingPayables { + PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: false, }, ); test_no_consuming_wallet_found( &subject, - HintableScanner::PendingPayables { + PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: true, }, ); @@ -682,7 +668,7 @@ mod tests { fn resolve_error_for_pending_payables_forbidden_states() { fn test_forbidden_states( subject: &ScanSchedulers, - inputs: &AllStartScanErrorsAdjustable, + inputs: &ListOfStartScanErrors, initial_pending_payable_scan: bool, ) { inputs.errors.iter().for_each(|error| { @@ -690,7 +676,7 @@ mod tests { subject .reschedule_on_error_resolver .resolve_rescheduling_for_given_error( - HintableScanner::PendingPayables { + PayableSequenceScanner::PendingPayables { initial_pending_payable_scan, }, *error, @@ -713,11 +699,10 @@ mod tests { }) } - let inputs = - AllStartScanErrorsAdjustable::default().eliminate_already_tested_variants(vec![ - StartScanError::NothingToProcess, - StartScanError::NoConsumingWalletFound, - ]); + let inputs = ListOfStartScanErrors::default().eliminate_already_tested_variants(vec![ + StartScanError::NothingToProcess, + StartScanError::NoConsumingWalletFound, + ]); let subject = ScanSchedulers::new(ScanIntervals::default(), true); test_forbidden_states(&subject, &inputs, false); @@ -728,7 +713,7 @@ mod tests { fn resolve_rescheduling_for_given_error_works_for_retry_payables_if_externally_triggered() { let subject = ScanSchedulers::new(ScanIntervals::default(), false); - test_what_if_externally_triggered(&subject, HintableScanner::RetryPayables {}); + test_what_if_externally_triggered(&subject, PayableSequenceScanner::RetryPayables {}); } #[test] @@ -740,7 +725,7 @@ mod tests { subject .reschedule_on_error_resolver .resolve_rescheduling_for_given_error( - HintableScanner::RetryPayables, + PayableSequenceScanner::RetryPayables, error, false, ) @@ -765,7 +750,7 @@ mod tests { fn resolve_rescheduling_for_given_error_works_for_new_payables_if_externally_triggered() { let subject = ScanSchedulers::new(ScanIntervals::default(), true); - test_what_if_externally_triggered(&subject, HintableScanner::NewPayables {}); + test_what_if_externally_triggered(&subject, PayableSequenceScanner::NewPayables {}); } #[test] @@ -779,7 +764,7 @@ mod tests { let _ = subject .reschedule_on_error_resolver .resolve_rescheduling_for_given_error( - HintableScanner::NewPayables, + PayableSequenceScanner::NewPayables, &StartScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Payables, started_at: SystemTime::now(), @@ -790,19 +775,22 @@ mod tests { #[test] fn resolve_hint_for_new_payables_with_those_error_cases_that_result_in_future_rescheduling() { - let inputs = - AllStartScanErrorsAdjustable::default().eliminate_already_tested_variants(vec![ - StartScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::Payables, - started_at: SystemTime::now(), - }, - ]); + let inputs = ListOfStartScanErrors::default().eliminate_already_tested_variants(vec![ + StartScanError::ScanAlreadyRunning { + pertinent_scanner: ScanType::Payables, + started_at: SystemTime::now(), + }, + ]); let subject = ScanSchedulers::new(ScanIntervals::default(), true); inputs.errors.iter().for_each(|error| { let result = subject .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error(HintableScanner::NewPayables, *error, false); + .resolve_rescheduling_for_given_error( + PayableSequenceScanner::NewPayables, + *error, + false, + ); assert_eq!( result, @@ -816,21 +804,21 @@ mod tests { #[test] fn conversion_between_hintable_scanner_and_scan_type_works() { assert_eq!( - ScanType::from(HintableScanner::NewPayables), + ScanType::from(PayableSequenceScanner::NewPayables), ScanType::Payables ); assert_eq!( - ScanType::from(HintableScanner::RetryPayables), + ScanType::from(PayableSequenceScanner::RetryPayables), ScanType::Payables ); assert_eq!( - ScanType::from(HintableScanner::PendingPayables { + ScanType::from(PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: false }), ScanType::PendingPayables ); assert_eq!( - ScanType::from(HintableScanner::PendingPayables { + ScanType::from(PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: true }), ScanType::PendingPayables diff --git a/node/src/test_utils/recorder_stop_conditions.rs b/node/src/test_utils/recorder_stop_conditions.rs index 6440b0289..9a3214eea 100644 --- a/node/src/test_utils/recorder_stop_conditions.rs +++ b/node/src/test_utils/recorder_stop_conditions.rs @@ -7,11 +7,11 @@ use std::any::{Any, TypeId}; pub enum StopConditions { Any(Vec), - // Msg is tested against every ID method in the vector. In every case when they match, those - // methods are eliminated + // Single message can eliminate _multiple_ ID Methods (previously stop conditions) by matching + // on them. AllGreedily(Vec), - // Iterating through the vector of ID methods, only the first matching method is eliminated - // and any other one that would've matched needs to wait for the next received msg of this type + // Single message can eliminate _only one_ ID Method (previously stop conditions) by matching + // on them. To remove others, a new message must be received. AllLazily(Vec), } @@ -111,9 +111,7 @@ impl MsgIdentification { pub fn resolve_condition + Send + 'static>(&self, msg: &Msg) -> bool { match self { MsgIdentification::ByType(type_id) => Self::matches_by_type::(msg, *type_id), - MsgIdentification::ByMatch { exemplar } => { - Self::matches_completely::(exemplar, msg) - } + MsgIdentification::ByMatch { exemplar } => Self::is_identical::(exemplar, msg), MsgIdentification::ByPredicate { predicate } => { Self::matches_by_predicate(predicate.as_ref(), msg) } @@ -125,7 +123,7 @@ impl MsgIdentification { trigger_msg_type_id == expected_type_id } - fn matches_completely + 'static + Send>( + fn is_identical + 'static + Send>( exemplar: &BoxedMsgExpected, msg: &Msg, ) -> bool { @@ -354,19 +352,11 @@ mod tests { let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_1); assert_eq!(kill_system, false); - match &cond_set { - StopConditions::AllGreedily(conds) => { - assert_eq!(conds.len(), 2); - assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); - assert!(matches!(conds[1], MsgIdentification::ByType(_))); - } - StopConditions::Any(_) => { - panic!("Stage 1: expected StopConditions::AllGreedily, not Any") - } - StopConditions::AllLazily(_) => { - panic!("Stage 1: expected StopConditions::AllGreedily, not AllLazily") - } - } + assert_state_after_greedily_matched(1, &cond_set, |conds| { + assert_eq!(conds.len(), 2); + assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); + assert!(matches!(conds[1], MsgIdentification::ByType(_))); + }); let tested_msg_2 = NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(1, 2, 4, 1)), }; @@ -374,15 +364,21 @@ mod tests { let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_2); assert_eq!(kill_system, true); + assert_state_after_greedily_matched(2, &cond_set, |conds| assert!(conds.is_empty())) + } + + fn assert_state_after_greedily_matched( + stage: usize, + cond_set: &StopConditions, + apply_assertions: fn(&[MsgIdentification]), + ) { match cond_set { - StopConditions::AllGreedily(conds) => { - assert!(conds.is_empty()) - } + StopConditions::AllGreedily(conds) => apply_assertions(conds), StopConditions::Any(_) => { - panic!("Stage 2: expected StopConditions::AllGreedily, not Any") + panic!("Stage {stage}: expected StopConditions::AllGreedily, not Any") } StopConditions::AllLazily(_) => { - panic!("Stage 2: expected StopConditions::AllGreedily, not AllLazily") + panic!("Stage {stage}: expected StopConditions::AllGreedily, not AllLazily") } } } @@ -402,6 +398,8 @@ mod tests { MsgIdentification::ByType(TypeId::of::()), MsgIdentification::ByType(TypeId::of::()), ]); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage one let tested_msg_1 = ScanForNewPayables { response_skeleton_opt: None, }; @@ -409,12 +407,14 @@ mod tests { let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_1); assert_eq!(kill_system, false); - assert_on_state_after_lazily_matched(1, &cond_set, |conds| { + assert_state_after_lazily_matched(1, &cond_set, |conds| { assert_eq!(conds.len(), 3); assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); assert!(matches!(conds[1], MsgIdentification::ByType(_))); assert!(matches!(conds[2], MsgIdentification::ByType(_))); }); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage two let tested_msg_2 = NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(6, 7, 8, 9)), }; @@ -422,11 +422,13 @@ mod tests { let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_2); assert_eq!(kill_system, false); - assert_on_state_after_lazily_matched(2, &cond_set, |conds| { + assert_state_after_lazily_matched(2, &cond_set, |conds| { assert_eq!(conds.len(), 2); assert!(matches!(conds[0], MsgIdentification::ByPredicate { .. })); assert!(matches!(conds[1], MsgIdentification::ByType(_))); }); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage three let tested_msg_3 = NewPublicIp { new_ip: IpAddr::V6(Ipv6Addr::new(1, 2, 4, 1, 4, 3, 2, 1)), }; @@ -434,10 +436,12 @@ mod tests { let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_3); assert_eq!(kill_system, false); - assert_on_state_after_lazily_matched(3, &cond_set, |conds| { + assert_state_after_lazily_matched(3, &cond_set, |conds| { assert_eq!(conds.len(), 1); assert!(matches!(conds[0], MsgIdentification::ByType(_))) }); + //////////////////////////////////////////////////////////////////////////////////////////// + // Stage four let tested_msg_4 = NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(45, 45, 45, 45)), }; @@ -445,26 +449,24 @@ mod tests { let kill_system = cond_set.resolve_stop_conditions::(&tested_msg_4); assert_eq!(kill_system, true); - assert_on_state_after_lazily_matched(4, &cond_set, |conds| { + assert_state_after_lazily_matched(4, &cond_set, |conds| { assert!(conds.is_empty()); }); } - fn assert_on_state_after_lazily_matched( + fn assert_state_after_lazily_matched( stage: usize, cond_set: &StopConditions, - assertions: fn(&[MsgIdentification]), + apply_assertions: fn(&[MsgIdentification]), ) { match &cond_set { - StopConditions::AllLazily(conds) => assertions(conds), - StopConditions::Any(_) => panic!( - "Stage {}: expected StopConditions::AllLazily, not Any", - stage - ), - StopConditions::AllGreedily(_) => panic!( - "Stage {}: expected StopConditions::AllLazily, not AllGreedily", - stage - ), + StopConditions::AllLazily(conds) => apply_assertions(conds), + StopConditions::Any(_) => { + panic!("Stage {stage}: expected StopConditions::AllLazily, not Any") + } + StopConditions::AllGreedily(_) => { + panic!("Stage {stage}: expected StopConditions::AllLazily, not AllGreedily") + } } } } From b4561362e3ff3fd793ca7afda493d03222db34e4 Mon Sep 17 00:00:00 2001 From: Bert Date: Tue, 3 Jun 2025 12:58:46 +0200 Subject: [PATCH 35/49] GH-602: addressing review continues on and on... --- node/src/accountant/mod.rs | 165 +++++++++--------- .../accountant/scanners/scan_schedulers.rs | 6 +- 2 files changed, 82 insertions(+), 89 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 6ae342391..f99fa57c5 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1650,12 +1650,12 @@ mod tests { agent_id_stamp ); assert_eq!(blockchain_bridge_recording.len(), 1); - test_use_of_the_same_logger(&logger_clone, test_name, None) + assert_using_the_same_logger(&logger_clone, test_name, None) // adjust_payments() did not need a prepared result, which means it wasn't reached // because otherwise this test would've panicked } - fn test_use_of_the_same_logger( + fn assert_using_the_same_logger( logger_clone: &Logger, test_name: &str, differentiation_opt: Option<&str>, @@ -1782,7 +1782,7 @@ mod tests { Some(response_skeleton) ); assert_eq!(blockchain_bridge_recording.len(), 1); - test_use_of_the_same_logger(&logger_clone, test_name, None) + assert_using_the_same_logger(&logger_clone, test_name, None) } #[test] @@ -2094,36 +2094,38 @@ mod tests { .build_and_provide_addresses(); let subject_addr = subject.start(); let system = System::new("test"); + let first_counter_msg_setup = setup_for_counter_msg_triggered_via_type_id!( + RequestTransactionReceipts, + ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash: make_tx_hash(234), + status: TxStatus::Failed + }), + make_pending_payable_fingerprint() + )], + response_skeleton_opt + }, + &subject_addr + ); + let second_counter_msg_setup = setup_for_counter_msg_triggered_via_type_id!( + QualifiedPayablesMessage, + SentPayables { + payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( + PendingPayable { + recipient_wallet: make_wallet("abc"), + hash: make_tx_hash(789) + } + )]), + response_skeleton_opt + }, + &subject_addr + ); peer_addresses .blockchain_bridge_addr .try_send(SetUpCounterMsgs::new(vec![ - setup_for_counter_msg_triggered_via_type_id!( - RequestTransactionReceipts, - ReportTransactionReceipts { - fingerprints_with_receipts: vec![( - TransactionReceiptResult::RpcResponse(TxReceipt { - transaction_hash: make_tx_hash(234), - status: TxStatus::Failed - }), - make_pending_payable_fingerprint() - )], - response_skeleton_opt - }, - &subject_addr - ), - setup_for_counter_msg_triggered_via_type_id!( - QualifiedPayablesMessage, - SentPayables { - payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct( - PendingPayable { - recipient_wallet: make_wallet("abc"), - hash: make_tx_hash(789) - } - )]), - response_skeleton_opt - }, - &subject_addr - ), + first_counter_msg_setup, + second_counter_msg_setup, ])) .unwrap(); subject_addr.try_send(BindMessage { peer_actors }).unwrap(); @@ -2317,7 +2319,7 @@ mod tests { let message = blockchain_bridge_recorder.get_record::(0); assert_eq!(message, &qualified_payables_msg); assert_eq!(blockchain_bridge_recorder.len(), 1); - test_use_of_the_same_logger(&actual_logger, test_name, None) + assert_using_the_same_logger(&actual_logger, test_name, None) } #[test] @@ -2688,14 +2690,16 @@ mod tests { scan_for_pending_payables_notify_later_params_arc .lock() .unwrap(); - // The part with executing the scan for NewPayables is deliberately omitted here, but - // the scan for pending payables would've been scheduled just after that + // The part of running the `NewPayableScanner` is deliberately omitted here, we stop + // the test right before that. Normally, the first occasion for scheduling + // the `PendingPayableScanner` would've lied no sooner than after the `NewPayableScan` + // finishes, having produced at least one blockchain transactions. assert!( scan_for_pending_payables_notify_later_params.is_empty(), "We did not expect to see another schedule for pending payables, but it happened {:?}", scan_for_pending_payables_notify_later_params ); - test_use_of_the_same_logger(&pp_logger, test_name, Some("pp")); + assert_using_the_same_logger(&pp_logger, test_name, Some("pp")); // Assertions on the payable scanner proper functioning // First, there is no functionality from the payable scanner actually running. // We only witness it to be scheduled. @@ -2746,7 +2750,7 @@ mod tests { "Should be already empty but was {:?}", receivable_start_scan_params ); - test_use_of_the_same_logger(&r_logger, test_name, Some("r")); + assert_using_the_same_logger(&r_logger, test_name, Some("r")); let scan_for_receivables_notify_later_params = scan_for_receivables_notify_later_params_arc.lock().unwrap(); assert_eq!( @@ -3098,7 +3102,7 @@ mod tests { assert!(time_before <= timestamp && timestamp <= time_after); assert_eq!(response_skeleton_opt, None); assert!(start_scan_payable_params.is_empty()); - debug!(logger, "verifying payable scanner logger"); + assert_using_the_same_logger(&logger, test_name, Some("abc")); let mut start_scan_pending_payable_params = start_scan_pending_payable_params_arc.lock().unwrap(); let (wallet, timestamp, response_skeleton_opt, logger, _) = @@ -3107,7 +3111,7 @@ mod tests { assert!(time_before <= timestamp && timestamp <= time_after); assert_eq!(response_skeleton_opt, None); assert!(start_scan_pending_payable_params.is_empty()); - debug!(logger, "verifying pending payable scanner logger"); + assert_using_the_same_logger(&logger, test_name, Some("def")); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); let actual_qualified_payables_msg = blockchain_bridge_recording.get_record::(0); @@ -4278,7 +4282,7 @@ mod tests { let mut finish_scan_params = finish_scan_params_arc.lock().unwrap(); let (actual_sent_payable, logger) = finish_scan_params.remove(0); assert_eq!(actual_sent_payable, sent_payable,); - test_use_of_the_same_logger(&logger, test_name, None); + assert_using_the_same_logger(&logger, test_name, None); let mut payable_notify_later_params = payable_notify_later_params_arc.lock().unwrap(); let (scheduled_msg, _interval) = payable_notify_later_params.remove(0); assert_eq!(scheduled_msg, ScanForNewPayables::default()); @@ -4311,7 +4315,7 @@ mod tests { Box::new(NotifyHandleMock::default().notify_params(&retry_payable_notify_params_arc)); let system = System::new(test_name); let (mut msg, _) = - make_report_transaction_receipts_msg(TxStatus::Pending, TxStatus::Failed); + make_report_transaction_receipts_msg(vec![TxStatus::Pending, TxStatus::Failed]); let response_skeleton_opt = Some(ResponseSkeleton { client_id: 45, context_id: 7, @@ -4333,7 +4337,7 @@ mod tests { response_skeleton_opt }] ); - test_use_of_the_same_logger(&logger, test_name, None) + assert_using_the_same_logger(&logger, test_name, None) } #[test] @@ -4375,7 +4379,7 @@ mod tests { subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); let subject_addr = subject.start(); - let (msg, two_fingerprints) = make_report_transaction_receipts_msg( + let (msg, two_fingerprints) = make_report_transaction_receipts_msg(vec![ TxStatus::Succeeded(TransactionBlock { block_hash: make_tx_hash(123), block_number: U64::from(100), @@ -4384,7 +4388,7 @@ mod tests { block_hash: make_tx_hash(234), block_number: U64::from(200), }), - ); + ]); subject_addr.try_send(msg).unwrap(); @@ -4451,7 +4455,7 @@ mod tests { subject.scan_schedulers.payable.new_payable_notify = Box::new(NotifyHandleMock::default().notify_params(&new_payable_notify_arc)); let subject_addr = subject.start(); - let (msg, two_fingerprints) = make_report_transaction_receipts_msg( + let (msg, two_fingerprints) = make_report_transaction_receipts_msg(vec![ TxStatus::Succeeded(TransactionBlock { block_hash: make_tx_hash(123), block_number: U64::from(100), @@ -4460,7 +4464,7 @@ mod tests { block_hash: make_tx_hash(234), block_number: U64::from(200), }), - ); + ]); subject_addr.try_send(msg).unwrap(); @@ -4515,7 +4519,7 @@ mod tests { NotifyLaterHandleMock::default().notify_later_params(&new_payable_notify_later_arc), ); let subject_addr = subject.start(); - let (msg, _) = make_report_transaction_receipts_msg( + let (msg, _) = make_report_transaction_receipts_msg(vec![ TxStatus::Succeeded(TransactionBlock { block_hash: make_tx_hash(123), block_number: U64::from(100), @@ -4524,7 +4528,7 @@ mod tests { block_hash: make_tx_hash(234), block_number: U64::from(200), }), - ); + ]); subject_addr.try_send(msg).unwrap(); @@ -4555,49 +4559,38 @@ mod tests { } fn make_report_transaction_receipts_msg( - status_of_tx_1: TxStatus, - status_of_tx_2: TxStatus, - ) -> (ReportTransactionReceipts, [PendingPayableFingerprint; 2]) { - let transaction_hash_1 = make_tx_hash(4545); - let transaction_receipt_1 = TxReceipt { - transaction_hash: transaction_hash_1, - status: status_of_tx_1, - }; - let fingerprint_1 = PendingPayableFingerprint { - rowid: 5, - timestamp: from_unix_timestamp(200_000_000), - hash: transaction_hash_1, - attempt: 2, - amount: 444, - process_error: None, - }; - let transaction_hash_2 = make_tx_hash(3333333); - let transaction_receipt_2 = TxReceipt { - transaction_hash: transaction_hash_2, - status: status_of_tx_2, - }; - let fingerprint_2 = PendingPayableFingerprint { - rowid: 10, - timestamp: from_unix_timestamp(199_780_000), - hash: Default::default(), - attempt: 15, - amount: 1212, - process_error: None, - }; - let msg = ReportTransactionReceipts { - fingerprints_with_receipts: vec![ - ( - TransactionReceiptResult::RpcResponse(transaction_receipt_1), - fingerprint_1.clone(), - ), + status_txs: Vec, + ) -> (ReportTransactionReceipts, Vec) { + let (receipt_result_fingerprint_pairs, fingerprints): (Vec<_>, Vec<_>) = status_txs + .into_iter() + .enumerate() + .map(|(idx, status)| { + let transaction_hash = make_tx_hash(idx as u32); + let transaction_receipt_result = TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash, + status, + }); + let fingerprint = PendingPayableFingerprint { + rowid: idx as u64, + timestamp: from_unix_timestamp(1_000_000_000 * idx as i64), + hash: transaction_hash, + attempt: 2, + amount: 1_000_000 * idx as u128 * idx as u128, + process_error: None, + }; ( - TransactionReceiptResult::RpcResponse(transaction_receipt_2), - fingerprint_2.clone(), - ), - ], + (transaction_receipt_result, fingerprint.clone()), + fingerprint, + ) + }) + .unzip(); + + let msg = ReportTransactionReceipts { + fingerprints_with_receipts: receipt_result_fingerprint_pairs, response_skeleton_opt: None, }; - (msg, [fingerprint_1, fingerprint_2]) + + (msg, fingerprints) } #[test] diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 727bd75a7..18487b4db 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -122,9 +122,9 @@ impl PayableScanScheduler { } } - // Can be triggered by a command, whereas the finished pending payable scanner is followed up - // by this scheduled message that can bear the response skeleton. This message is inserted into - // the Accountant's mailbox with no delay + // This message is always inserted into the Accountant's mailbox with no delay. + // Can also be triggered by command, following up after the PendingPayableScanner that + // requests it. That's why the response skeleton is possible to be used. pub fn schedule_retry_payable_scan( &self, ctx: &mut Context, From 2940b6bb93a7fae359604879e10afe55cceb72f4 Mon Sep 17 00:00:00 2001 From: Bert Date: Wed, 4 Jun 2025 00:06:10 +0200 Subject: [PATCH 36/49] GH-602: more things for the review... --- node/src/accountant/scanners/mod.rs | 249 ++++++++++-------- .../accountant/scanners/scan_schedulers.rs | 8 +- node/src/accountant/scanners/test_utils.rs | 39 +-- 3 files changed, 174 insertions(+), 122 deletions(-) diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 6e18825f1..9702d074b 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -131,7 +131,7 @@ impl Scanners { } if let Some(started_at) = self.payable.scan_started_at() { return Err(StartScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::Payables, + cross_scan_cause_opt: None, started_at, }); } @@ -203,13 +203,13 @@ impl Scanners { } (Some(started_at), None) => { return Err(StartScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::PendingPayables, + cross_scan_cause_opt: None, started_at, }) } (None, Some(started_at)) => { return Err(StartScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::Payables, + cross_scan_cause_opt: Some(ScanType::Payables), started_at, }) } @@ -235,7 +235,7 @@ impl Scanners { } if let Some(started_at) = self.receivable.scan_started_at() { return Err(StartScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::Receivables, + cross_scan_cause_opt: None, started_at, }); } @@ -1277,7 +1277,7 @@ pub enum StartScanError { NothingToProcess, NoConsumingWalletFound, ScanAlreadyRunning { - pertinent_scanner: ScanType, + cross_scan_cause_opt: Option, started_at: SystemTime, }, CalledFromNullScanner, // Exclusive for tests @@ -1297,10 +1297,11 @@ impl StartScanError { scan_type )), StartScanError::ScanAlreadyRunning { - pertinent_scanner, + cross_scan_cause_opt, started_at, } => ErrorType::Temporary(Self::scan_already_running_msg( - *pertinent_scanner, + scan_type, + *cross_scan_cause_opt, *started_at, )), StartScanError::NoConsumingWalletFound => ErrorType::Permanent(format!( @@ -1330,15 +1331,12 @@ impl StartScanError { }, }; - match is_externally_triggered { - true => match log_message { - ErrorType::Temporary(msg) => info!(logger, "{}", msg), - ErrorType::Permanent(msg) => warning!(logger, "{}", msg), - }, - - false => match log_message { - ErrorType::Temporary(msg) | ErrorType::Permanent(msg) => debug!(logger, "{}", msg), + match log_message { + ErrorType::Temporary(msg) => match is_externally_triggered { + true => info!(logger, "{}", msg), + false => debug!(logger, "{}", msg), }, + ErrorType::Permanent(msg) => warning!(logger, "{}", msg), } } @@ -1353,13 +1351,22 @@ impl StartScanError { } fn scan_already_running_msg( - error_pertinent_scanner: ScanType, - error_pertinent_scanner_started: SystemTime, + request_of: ScanType, + cross_scan_cause_opt: Option, + scan_started: SystemTime, ) -> String { + let (blocking_scanner, request_spec) = if let Some(cross_scan_cause) = cross_scan_cause_opt + { + (cross_scan_cause, format!("the {:?}", request_of)) + } else { + (request_of, "this".to_string()) + }; + format!( - "{:?} scan was already initiated at {}. Hence, this scan request will be ignored.", - error_pertinent_scanner, - StartScanError::timestamp_as_string(error_pertinent_scanner_started) + "{:?} scan was already initiated at {}. Hence, {} scan request will be ignored.", + blocking_scanner, + StartScanError::timestamp_as_string(scan_started), + request_spec ) } } @@ -1370,6 +1377,16 @@ pub enum MTError { UnnecessaryRequest { hint_opt: Option }, } +pub trait RealScannerMarker {} + +macro_rules! impl_real_scanner_marker { + ($($t:ty),*) => { + $(impl RealScannerMarker for $t {})* + } +} + +impl_real_scanner_marker!(PayableScanner, PendingPayableScanner, ReceivableScanner); + // Note that this location was chosen because the following mocks need to implement a private trait // from this file #[cfg(test)] @@ -1664,6 +1681,10 @@ pub mod local_test_utils { intentionally_blank!() } } + + pub trait ScannerMockMarker {} + + impl ScannerMockMarker for ScannerMock {} } #[cfg(test)] @@ -1974,7 +1995,7 @@ mod tests { assert_eq!( result, Err(StartScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::Payables, + cross_scan_cause_opt: None, started_at: previous_scan_started_at }) ); @@ -3150,7 +3171,7 @@ mod tests { assert_eq!( result, Err(StartScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::PendingPayables, + cross_scan_cause_opt: None, started_at: now }) ); @@ -3183,7 +3204,7 @@ mod tests { assert_eq!( result, Err(StartScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::Payables, + cross_scan_cause_opt: Some(ScanType::Payables), started_at: previous_scan_started_at }) ); @@ -3997,7 +4018,7 @@ mod tests { assert_eq!( result, Err(StartScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::Receivables, + cross_scan_cause_opt: None, started_at: now }) ); @@ -4424,24 +4445,50 @@ mod tests { } #[test] - fn scan_already_running_msg_displays_correctly() { - let still_running_scanner = ScanType::PendingPayables; + fn scan_already_running_msg_displays_correctly_if_blocked_by_requested_scan() { + test_scan_already_running_msg( + ScanType::PendingPayables, + None, + "PendingPayables scan was already initiated at", + ". Hence, this scan request will be ignored.", + ) + } + + #[test] + fn scan_already_running_msg_displays_correctly_if_blocked_by_other_scan_than_directly_requested( + ) { + test_scan_already_running_msg( + ScanType::PendingPayables, + Some(ScanType::Payables), + "Payables scan was already initiated at", + ". Hence, the PendingPayables scan request will be ignored.", + ) + } + + fn test_scan_already_running_msg( + requested_scan: ScanType, + cross_scan_blocking_cause_opt: Option<(ScanType)>, + expected_leading_msg_fragment: &str, + expectd_trailing_msg_fragment: &str, + ) { let time = SystemTime::now(); - let result = StartScanError::scan_already_running_msg(still_running_scanner, time); + let result = StartScanError::scan_already_running_msg( + requested_scan, + cross_scan_blocking_cause_opt, + time, + ); - let expected_first_fragment = "PendingPayables scan was already initiated at"; assert!( - result.contains(expected_first_fragment), + result.contains(expected_leading_msg_fragment), "We expected {} but the msg is: {}", - expected_first_fragment, + expected_leading_msg_fragment, result ); - let expected_second_fragment = ". Hence, this scan request will be ignored."; assert!( - result.contains(expected_second_fragment), + result.contains(expectd_trailing_msg_fragment), "We expected {} but the msg is: {}", - expected_second_fragment, + expectd_trailing_msg_fragment, result ); let regex = PseudoTimestamp::regex(); @@ -4521,106 +4568,102 @@ mod tests { } #[test] - fn log_error_works_fine_for_automatic_scanning() { + fn log_error_works_fine() { init_test_logging(); - let test_name = "log_error_works_fine_for_automatic_scanning"; + let test_name = "log_error_works_fine"; let now = SystemTime::now(); - let input = vec![ + let input: Vec<(StartScanError, Box String>, &str, &str)> = vec![ ( StartScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::Payables, - started_at: now + cross_scan_cause_opt: None, + started_at: now, }, - format!("DEBUG: {test_name}: Payables scan was already initiated at {}", StartScanError::timestamp_as_string(now)) - ), - ( - StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), - format!("DEBUG: {test_name}: User requested Payables scan was denied. Automatic mode prevents manual triggers.") - ), - ( - StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { - hint_opt: Some("Wise words".to_string()) + Box::new(|sev| { + format!( + "{sev}: {test_name}: Payables scan was already initiated at {}", + StartScanError::timestamp_as_string(now) + ) }), - format!("DEBUG: {test_name}: User requested Payables scan was denied expecting zero findings. Wise words") - ), - ( - StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { - hint_opt: None} - ), - format!("DEBUG: {test_name}: User requested Payables scan was denied expecting zero findings.") - ), - ( - StartScanError::CalledFromNullScanner, - format!("DEBUG: {test_name}: Called from NullScanner, not the Payables scanner.") - ), - ( - StartScanError::NoConsumingWalletFound, - format!("DEBUG: {test_name}: Cannot initiate Payables scan because no consuming wallet was found.") - ), - ( - StartScanError::NothingToProcess, - format!("DEBUG: {test_name}: There was nothing to process during Payables scan.") - ), - ]; - let logger = Logger::new(test_name); - let test_log_handler = TestLogHandler::new(); - - input.into_iter().for_each(|(err, expected_log_msg)| { - err.log_error(&logger, ScanType::Payables, false); - - test_log_handler.exists_log_containing(&expected_log_msg); - }); - } - - #[test] - fn log_error_works_fine_for_externally_triggered_scanning() { - init_test_logging(); - let test_name = "log_error_works_fine_for_externally_triggered_scanning"; - let input = vec![ - ( - StartScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::Payables, - started_at: SystemTime::now() - }, - format!("INFO: {test_name}: Payables scan was already initiated at" /*TODO suboptimal */) + "INFO", + "DEBUG", ), ( StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), - format!("WARN: {test_name}: User requested Payables scan was denied. Automatic mode prevents manual triggers.") + Box::new(|sev| { + format!("{sev}: {test_name}: User requested Payables scan was denied. Automatic mode prevents manual triggers.") + }), + "WARN", + "WARN", ), ( StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { - hint_opt: Some("Wise words".to_string()) + hint_opt: Some("Wise words".to_string()), + }), + Box::new(|sev| { + format!("{sev}: {test_name}: User requested Payables scan was denied expecting zero findings. Wise words") }), - format!("INFO: {test_name}: User requested Payables scan was denied expecting zero findings. Wise words") + "INFO", + "DEBUG", ), ( - StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { - hint_opt: None} - ), - format!("INFO: {test_name}: User requested Payables scan was denied expecting zero findings.") + StartScanError::ManualTriggerError(MTError::UnnecessaryRequest { hint_opt: None }), + Box::new(|sev| { + format!("{sev}: {test_name}: User requested Payables scan was denied expecting zero findings.") + }), + "INFO", + "DEBUG", ), ( StartScanError::CalledFromNullScanner, - format!("WARN: {test_name}: Called from NullScanner, not the Payables scanner.") + Box::new(|sev| { + format!( + "{sev}: {test_name}: Called from NullScanner, not the Payables scanner." + ) + }), + "WARN", + "WARN", ), ( StartScanError::NoConsumingWalletFound, - format!("WARN: {test_name}: Cannot initiate Payables scan because no consuming wallet was found.") + Box::new(|sev| { + format!("{sev}: {test_name}: Cannot initiate Payables scan because no consuming wallet was found.") + }), + "WARN", + "WARN", ), ( StartScanError::NothingToProcess, - format!("INFO: {test_name}: There was nothing to process during Payables scan.") + Box::new(|sev| { + format!( + "{sev}: {test_name}: There was nothing to process during Payables scan." + ) + }), + "INFO", + "DEBUG", ), ]; let logger = Logger::new(test_name); let test_log_handler = TestLogHandler::new(); - input.into_iter().for_each(|(err, expected_log_msg)| { - err.log_error(&logger, ScanType::Payables, true); + input.into_iter().for_each( + |( + err, + form_expected_log_msg, + severity_for_externally_triggered_scans, + severity_for_automatic_scans, + )| { + err.log_error(&logger, ScanType::Payables, true); - test_log_handler.exists_log_containing(&expected_log_msg); - }); + let expected_log_msg = + form_expected_log_msg(severity_for_externally_triggered_scans); + test_log_handler.exists_log_containing(&expected_log_msg); + + err.log_error(&logger, ScanType::Payables, false); + + let expected_log_msg = form_expected_log_msg(severity_for_automatic_scans); + test_log_handler.exists_log_containing(&expected_log_msg); + }, + ); } fn make_dull_subject() -> Scanners { diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 18487b4db..df90adea4 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -483,7 +483,7 @@ mod tests { let candidates = vec![ StartScanError::NothingToProcess, StartScanError::NoConsumingWalletFound, - StartScanError::ScanAlreadyRunning { pertinent_scanner: ScanType::Payables, started_at: SystemTime::now()}, + StartScanError::ScanAlreadyRunning { cross_scan_cause_opt: None, started_at: SystemTime::now()}, StartScanError::ManualTriggerError(MTError::AutomaticScanConflict), StartScanError::CalledFromNullScanner ]; @@ -766,7 +766,7 @@ mod tests { .resolve_rescheduling_for_given_error( PayableSequenceScanner::NewPayables, &StartScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::Payables, + cross_scan_cause_opt: None, started_at: SystemTime::now(), }, false, @@ -774,10 +774,10 @@ mod tests { } #[test] - fn resolve_hint_for_new_payables_with_those_error_cases_that_result_in_future_rescheduling() { + fn resolve_hint_for_new_payables_with_error_cases_resulting_in_future_rescheduling() { let inputs = ListOfStartScanErrors::default().eliminate_already_tested_variants(vec![ StartScanError::ScanAlreadyRunning { - pertinent_scanner: ScanType::Payables, + cross_scan_cause_opt: None, started_at: SystemTime::now(), }, ]); diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 878e33465..c6f9ca5e3 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -2,12 +2,14 @@ #![cfg(test)] -use crate::accountant::scanners::local_test_utils::ScannerMock; +use crate::accountant::scanners::local_test_utils::{ScannerMock, ScannerMockMarker}; use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; use crate::accountant::scanners::scan_schedulers::NewPayableScanDynIntervalComputer; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; -use crate::accountant::scanners::{PayableScanner, PendingPayableScanner, ReceivableScanner}; +use crate::accountant::scanners::{ + PayableScanner, PendingPayableScanner, RealScannerMarker, ReceivableScanner, +}; use crate::accountant::{ ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, SentPayables, }; @@ -55,34 +57,41 @@ impl NewPayableScanDynIntervalComputerMock { } } -pub enum ReplacementType { +pub enum ReplacementType +where + ScannerReal: RealScannerMarker, + ScannerMock: ScannerMockMarker, +{ Real(ScannerReal), - Mock(ScannerMock), + Mock(ScannerMock), Null, } -// The supplied scanner types are broken down to these detailed categories because they become -// eventually trait objects represented by a private trait. That one cannot be supplied, though, -// because it is unknown to the outside world, therefore, we provide the specific objects that -// will be then treated in an abstract manner. +// The scanners are categorized by types because we want them to become an abstract object +// represented by a private trait. Of course, such an object cannot be constructed directly in +// the outer world; therefore, we have to provide specific objects that will cast accordingly +// under the hood. pub enum ScannerReplacement { Payable( - ReplacementType, + ReplacementType< + PayableScanner, + ScannerMock, + >, ), PendingPayable( ReplacementType< PendingPayableScanner, - RequestTransactionReceipts, - ReportTransactionReceipts, - PendingPayableScanResult, + ScannerMock< + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, >, ), Receivable( ReplacementType< ReceivableScanner, - RetrieveTransactions, - ReceivedPayments, - Option, + ScannerMock>, >, ), } From cd0919c4a20b44505bab0a8b0af3085d0e1c67ed Mon Sep 17 00:00:00 2001 From: Bert Date: Wed, 4 Jun 2025 01:57:52 +0200 Subject: [PATCH 37/49] GH-602: last 6 comments to address --- node/Cargo.toml | 2 +- node/src/accountant/scanners/mod.rs | 128 ++++-------------- .../accountant/scanners/scan_schedulers.rs | 2 +- node/src/accountant/scanners/test_utils.rs | 60 +++++++- 4 files changed, 86 insertions(+), 106 deletions(-) diff --git a/node/Cargo.toml b/node/Cargo.toml index 93f2dcde1..b6d91b0cd 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -15,7 +15,7 @@ automap = { path = "../automap"} backtrace = "0.3.57" base64 = "0.13.0" bytes = "0.4.12" -time = {version = "0.3.11", features = [ "macros" ]} +time = {version = "0.3.11", features = [ "macros", "parsing" ]} clap = "2.33.3" crossbeam-channel = "0.5.1" dirs = "4.0.0" diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 9702d074b..7cc4f42e4 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -1721,10 +1721,9 @@ mod tests { use ethereum_types::U64; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use regex::{CaptureMatches, Regex}; + use regex::{Regex}; use rusqlite::{ffi, ErrorCode}; use std::cell::RefCell; - use std::cmp::Ordering; use std::collections::HashSet; use std::ops::Sub; use std::panic::{catch_unwind, AssertUnwindSafe}; @@ -1736,7 +1735,7 @@ mod tests { use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeToUiMessage; use crate::accountant::scanners::local_test_utils::{NullScanner}; - use crate::accountant::scanners::test_utils::{MarkScanner, ReplacementType, ScannerReplacement}; + use crate::accountant::scanners::test_utils::{assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, ReplacementType, ScannerReplacement}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; impl Scanners { @@ -2132,28 +2131,25 @@ mod tests { before: SystemTime, after: SystemTime, ) { - let regex = PseudoTimestamp::regex(); - let mut captures = regex.captures_iter(panic_msg); - let first_actual_as_pseudo_t = PseudoTimestamp::new_from_captures(&mut captures); - let second_actual_as_pseudo_t = PseudoTimestamp::new_from_captures(&mut captures); - let before_as_pseudo_t = PseudoTimestamp::from(before); - let after_as_pseudo_t = PseudoTimestamp::from(after); + let system_times = parse_system_time_from_str(panic_msg); + let first_actual = system_times[0]; + let second_actual = system_times[1]; assert!( - before_as_pseudo_t <= first_actual_as_pseudo_t - && first_actual_as_pseudo_t <= second_actual_as_pseudo_t - && second_actual_as_pseudo_t <= after_as_pseudo_t, + before <= first_actual + && first_actual <= second_actual + && second_actual <= after, "We expected this relationship before({:?}) <= first_actual({:?}) <= second_actual({:?}) \ <= after({:?}), but it does not hold true", - before_as_pseudo_t, - first_actual_as_pseudo_t, - second_actual_as_pseudo_t, - after_as_pseudo_t + before, + first_actual, + second_actual, + after ); } #[test] - #[should_panic(expected = "bluh")] + #[should_panic(expected = "Complete me with GH-605")] fn retry_payable_scanner_panics_in_case_no_qualified_payable_is_found() { let consuming_wallet = make_paying_wallet(b"consuming wallet"); let now = SystemTime::now(); @@ -3260,18 +3256,12 @@ mod tests { expected_msg_fragment_2, panic_msg ); - let regex = PseudoTimestamp::regex(); - let mut captures = regex.captures_iter(panic_msg); - let pseudo_timestamp_for_pending_payable_start = - PseudoTimestamp::new_from_captures(&mut captures); - let pseudo_timestamp_for_payable_start = PseudoTimestamp::new_from_captures(&mut captures); - assert_eq!( - pseudo_timestamp_for_pending_payable_start, - PseudoTimestamp::from(timestamp_pending_payable_start) - ); - assert_eq!( - pseudo_timestamp_for_payable_start, - PseudoTimestamp::from(timestamp_payable_scanner_start) + assert_timestamps_from_str( + panic_msg, + vec![ + timestamp_pending_payable_start, + timestamp_payable_scanner_start, + ], ) } @@ -4467,16 +4457,16 @@ mod tests { fn test_scan_already_running_msg( requested_scan: ScanType, - cross_scan_blocking_cause_opt: Option<(ScanType)>, + cross_scan_blocking_cause_opt: Option, expected_leading_msg_fragment: &str, - expectd_trailing_msg_fragment: &str, + expected_trailing_msg_fragment: &str, ) { - let time = SystemTime::now(); + let some_time = SystemTime::now(); let result = StartScanError::scan_already_running_msg( requested_scan, cross_scan_blocking_cause_opt, - time, + some_time, ); assert!( @@ -4486,19 +4476,12 @@ mod tests { result ); assert!( - result.contains(expectd_trailing_msg_fragment), + result.contains(expected_trailing_msg_fragment), "We expected {} but the msg is: {}", - expectd_trailing_msg_fragment, + expected_trailing_msg_fragment, result ); - let regex = PseudoTimestamp::regex(); - let mut captures = regex.captures_iter(&result); - let pseudo_timestamp_for_pending_payable_start = - PseudoTimestamp::new_from_captures(&mut captures); - assert_eq!( - pseudo_timestamp_for_pending_payable_start, - PseudoTimestamp::from(time) - ); + assert_timestamps_from_str(&result, vec![some_time]); } #[test] @@ -4675,63 +4658,4 @@ mod tests { receivable: Box::new(NullScanner::new()), } } - - // Concatenated hours, minutes, seconds and milliseconds in a single integer - #[derive(PartialEq, Debug)] - struct PseudoTimestamp { - rep: u32, - } - - // This is a one-hour difference, indicating wrapping around the midnight - const MIDNIGHT_INDICATIVE_DIFFERENCE_DIFF: u32 = 1_000_000; - - impl PartialOrd for PseudoTimestamp { - fn partial_cmp(&self, other: &Self) -> Option { - if self.rep == other.rep { - Some(Ordering::Equal) - } else if self.rep < other.rep { - if (other.rep - self.rep) > MIDNIGHT_INDICATIVE_DIFFERENCE_DIFF { - Some(Ordering::Greater) - } else { - Some(Ordering::Less) - } - } else { - if (self.rep - other.rep) > MIDNIGHT_INDICATIVE_DIFFERENCE_DIFF { - Some(Ordering::Less) - } else { - Some(Ordering::Greater) - } - } - } - } - - impl From for PseudoTimestamp { - fn from(timestamp: SystemTime) -> Self { - let specially_formatted_timestamp = StartScanError::timestamp_as_string(timestamp); - let regex = Self::regex(); - let mut captures = regex.captures_iter(&specially_formatted_timestamp); - PseudoTimestamp::new_from_captures(&mut captures) - } - } - - impl PseudoTimestamp { - fn new_from_captures(captures: &mut CaptureMatches) -> Self { - let captured_first_time = captures.next().unwrap().get(1).unwrap().as_str(); - let num = Self::remove_colons_and_dots(captured_first_time); - Self { - rep: u32::from_str_radix(&num, 10).unwrap(), - } - } - - fn regex() -> Regex { - Regex::new(r"\d{4}-\d{2}-\d{2} (\d{2}:\d{2}:\d{2}\.\d{3})").unwrap() - } - - fn remove_colons_and_dots(str: &str) -> String { - let mut str = str.to_string(); - str = str.replace(":", ""); - str = str.replace(".", ""); - str - } - } } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index df90adea4..e0724cf5f 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -756,7 +756,7 @@ mod tests { #[test] #[should_panic( expected = "internal error: entered unreachable code: an automatic scan of NewPayableScanner \ - should never interfere with itself ScanAlreadyRunning { pertinent_scanner: Payables, started_at:" + should never interfere with itself ScanAlreadyRunning { cross_scan_cause_opt: None, started_at:" )] fn resolve_hint_for_new_payables_if_scan_is_already_running_error_and_is_automatic_scan() { let subject = ScanSchedulers::new(ScanIntervals::default(), true); diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index c6f9ca5e3..5bde2ac7d 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -14,11 +14,14 @@ use crate::accountant::{ ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, SentPayables, }; use crate::blockchain::blockchain_bridge::RetrieveTransactions; -use masq_lib::logger::Logger; +use masq_lib::logger::{Logger, TIME_FORMATTING_STRING}; use masq_lib::ui_gateway::NodeToUiMessage; +use regex::Regex; use std::cell::RefCell; +use std::panic::panic_any; use std::sync::{Arc, Mutex}; -use std::time::{Duration, SystemTime}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use time::{format_description, OffsetDateTime, PrimitiveDateTime}; #[derive(Default)] pub struct NewPayableScanDynIntervalComputerMock { @@ -100,3 +103,56 @@ pub enum MarkScanner<'a> { Ended(&'a Logger), Started(SystemTime), } + +// Cautious: Don't compare to another timestamp on a full match; this timestamp is trimmed in +// nanoseconds down to three digits +pub fn parse_system_time_from_str(examined_str: &str) -> Vec { + let regex = Regex::new(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})").unwrap(); + let captures = regex.captures_iter(examined_str); + captures + .map(|captures| { + let captured_str_timestamp = captures.get(0).unwrap().as_str(); + let format = format_description::parse(TIME_FORMATTING_STRING).unwrap(); + let dt = PrimitiveDateTime::parse(captured_str_timestamp, &format).unwrap(); + let duration = Duration::from_secs(dt.assume_utc().unix_timestamp() as u64) + + Duration::from_nanos(dt.nanosecond() as u64); + UNIX_EPOCH + duration + }) + .collect() +} + +fn trim_expected_timestamp_to_three_digits_nanos(value: SystemTime) -> SystemTime { + let duration = value.duration_since(UNIX_EPOCH).unwrap(); + let full_nanos = duration.subsec_nanos(); + let diffuser = 10_u32.pow(6); + let trimmed_nanos = (full_nanos / diffuser) * diffuser; + let duration = duration + .checked_sub(Duration::from_nanos(full_nanos as u64)) + .unwrap() + .checked_add(Duration::from_nanos(trimmed_nanos as u64)) + .unwrap(); + UNIX_EPOCH + duration +} + +pub fn assert_timestamps_from_str(examined_str: &str, expected_timestamps: Vec) { + let parsed_timestamps = parse_system_time_from_str(examined_str); + if parsed_timestamps.len() != expected_timestamps.len() { + panic!( + "You supplied {} expected timestamps, but the examined text contains only {}", + expected_timestamps.len(), + parsed_timestamps.len() + ) + } + let zipped = parsed_timestamps + .into_iter() + .zip(expected_timestamps.into_iter()); + zipped.for_each(|(parsed_timestamp, expected_timestamp)| { + let expected_timestamp_trimmed = + trim_expected_timestamp_to_three_digits_nanos(expected_timestamp); + assert_eq!( + parsed_timestamp, expected_timestamp_trimmed, + "We expected this timestamp {:?} in this fragment '{}' but found {:?}", + expected_timestamp_trimmed, examined_str, parsed_timestamp + ) + }) +} From a271e8f76a470846940a53d6b7adeaf6125e2c32 Mon Sep 17 00:00:00 2001 From: Bert Date: Thu, 5 Jun 2025 16:06:51 +0200 Subject: [PATCH 38/49] GH-602: finished the big two tests covering the very start of the automatic process with pp scanner playing the first violin --- node/src/accountant/mod.rs | 547 +++++++++++++++++-- node/src/accountant/scanners/mod.rs | 4 +- node/src/accountant/scanners/test_utils.rs | 3 +- node/src/blockchain/blockchain_bridge.rs | 10 +- node/src/test_utils/recorder_counter_msgs.rs | 2 + 5 files changed, 505 insertions(+), 61 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index f99fa57c5..f93a7ee96 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -438,7 +438,7 @@ pub trait SkeletonOptHolder { #[derive(Debug, PartialEq, Eq, Message, Clone)] pub struct RequestTransactionReceipts { - pub pending_payable: Vec, + pub pending_payable_fingerprints: Vec, pub response_skeleton_opt: Option, } @@ -1248,7 +1248,7 @@ mod tests { use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use masq_lib::ui_gateway::MessagePath::Conversation; use masq_lib::ui_gateway::{MessageBody, MessagePath, NodeFromUiMessage, NodeToUiMessage}; - use std::any::TypeId; + use std::any::{TypeId}; use std::ops::{Sub}; use std::sync::Arc; use std::sync::Mutex; @@ -1836,7 +1836,7 @@ mod tests { assert_eq!( blockchain_bridge_recording.get_record::(0), &RequestTransactionReceipts { - pending_payable: vec![fingerprint], + pending_payable_fingerprints: vec![fingerprint], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321, @@ -2577,11 +2577,12 @@ mod tests { } #[test] - fn accountant_scans_after_startup() { - // We do ensure the PendingPayableScanner runs before the NewPayableScanner. It does not - // matter a lot when does the ReceivableScanner take place. + fn accountant_scans_after_startup_and_does_not_detect_straggled_pending_payables() { + // We do ensure the PendingPayableScanner runs before the NewPayableScanner. Not interested + // in an exact placing of the ReceivableScanner too much. init_test_logging(); - let test_name = "accountant_scans_after_startup"; + let test_name = + "accountant_scans_after_startup_and_does_not_detect_straggled_pending_payables"; let pending_payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); let receivable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); let payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); @@ -2593,7 +2594,7 @@ mod tests { let scan_for_receivables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let earning_wallet = make_wallet("earning"); let consuming_wallet = make_wallet("consuming"); - let system = System::new("accountant_scans_after_startup"); + let system = System::new(test_name); let _ = SystemKillerActor::new(Duration::from_secs(10)).start(); let config = bc_from_wallets(consuming_wallet.clone(), earning_wallet.clone()); let pending_payable_scanner = ScannerMock::new() @@ -2608,6 +2609,222 @@ mod tests { .scan_started_at_result(None) .start_scan_params(&payable_start_scan_params_arc) .start_scan_result(Err(StartScanError::NothingToProcess)); + let (subject, new_payable_expected_computed_interval, receivable_scan_interval) = + set_up_subject_for_no_pending_payables_found_startup_test( + test_name, + &scan_for_pending_payables_notify_later_params_arc, + &scan_for_new_payables_notify_later_params_arc, + &scan_for_new_payables_notify_params_arc, + &scan_for_retry_payables_notify_params_arc, + &compute_interval_params_arc, + &scan_for_receivables_notify_later_params_arc, + config, + pending_payable_scanner, + receivable_scanner, + payable_scanner, + ); + let peer_actors = peer_actors_builder().build(); + let subject_addr: Addr = subject.start(); + let subject_subs = Accountant::make_subs_from(&subject_addr); + send_bind_message!(subject_subs, peer_actors); + + send_start_message!(subject_subs); + + // The system is stopped by the NotifyLaterHandleMock for the Receivable scanner + system.run(); + let pp_scan_started_at = assert_pending_payable_scanner_for_no_pending_payable_found( + test_name, + consuming_wallet, + pending_payable_start_scan_params_arc, + scan_for_pending_payables_notify_later_params_arc, + ); + let p_scheduling_happened_at = assert_payable_scanner_for_no_pending_payable_found( + scan_for_new_payables_notify_later_params_arc, + new_payable_expected_computed_interval, + scan_for_new_payables_notify_params_arc, + scan_for_retry_payables_notify_params_arc, + compute_interval_params_arc, + ); + assert_receivable_scanner( + test_name, + earning_wallet, + receivable_start_scan_params_arc, + receivable_scan_interval, + scan_for_receivables_notify_later_params_arc, + ); + // We expect the PendingPayableScanner to take place before the PayableScanner. + // I do believe it's impossible for these two events to happen within the same nanosecond, + // until somebody fiddles with MASQ tests on a true supercomputer. + assert!( + pp_scan_started_at < p_scheduling_happened_at, + "We failed to prove that the PendingPayableScanner runs before the PayableScanner." + ); + } + + #[test] + fn accountant_scans_after_startup_and_detects_straggled_pending_payable() { + // We do ensure the PendingPayableScanner runs before the NewPayableScanner. Not interested + // in an exact placing of the ReceivableScanner too much. + init_test_logging(); + let test_name = "accountant_scans_after_startup_and_detects_straggled_pending_payable"; + let pending_payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); + let pending_payable_finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let receivable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); + let payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); + let payable_finish_scan_params_arc = Arc::new(Mutex::new(vec![])); + let scan_for_pending_payables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let scan_for_new_payables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let scan_for_new_payables_notify_params_arc = Arc::new(Mutex::new(vec![])); + let scan_for_retry_payables_notify_params_arc = Arc::new(Mutex::new(vec![])); + let scan_for_receivables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let earning_wallet = make_wallet("earning"); + let consuming_wallet = make_wallet("consuming"); + let system = System::new(test_name); + let _ = SystemKillerActor::new(Duration::from_secs(10)).start(); + let config = bc_from_wallets(consuming_wallet.clone(), earning_wallet.clone()); + let pp_fingerprint = make_pending_payable_fingerprint(); + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&pending_payable_start_scan_params_arc) + .start_scan_result(Ok(RequestTransactionReceipts { + pending_payable_fingerprints: vec![pp_fingerprint], + response_skeleton_opt: None, + })) + .finish_scan_params(&pending_payable_finish_scan_params_arc) + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + let receivable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&receivable_start_scan_params_arc) + .start_scan_result(Err(StartScanError::NothingToProcess)); + let payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .scan_started_at_result(None) + .start_scan_params(&payable_start_scan_params_arc) + .start_scan_result(Ok(QualifiedPayablesMessage { + qualified_payables: vec![make_payable_account(123)], + consuming_wallet: consuming_wallet.clone(), + response_skeleton_opt: None, + })) + .finish_scan_params(&payable_finish_scan_params_arc) + // Important + .finish_scan_result(PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::NewPendingPayable, + }); + let (subject, pending_payable_expected_notify_later_interval, receivable_scan_interval) = + set_up_subject_for_some_pending_payable_found_startup_test( + test_name, + &scan_for_pending_payables_notify_later_params_arc, + &scan_for_new_payables_notify_later_params_arc, + &scan_for_new_payables_notify_params_arc, + &scan_for_retry_payables_notify_params_arc, + &scan_for_receivables_notify_later_params_arc, + config, + pending_payable_scanner, + receivable_scanner, + payable_scanner, + ); + let (peer_actors, addresses) = peer_actors_builder().build_and_provide_addresses(); + let subject_addr: Addr = subject.start(); + let subject_subs = Accountant::make_subs_from(&subject_addr); + let expected_report_transaction_receipts = ReportTransactionReceipts { + fingerprints_with_receipts: vec![( + TransactionReceiptResult::RpcResponse(TxReceipt { + transaction_hash: make_tx_hash(789), + status: TxStatus::Failed, + }), + make_pending_payable_fingerprint(), + )], + response_skeleton_opt: None, + }; + let expected_sent_payables = SentPayables { + payment_procedure_result: Ok(vec![ProcessedPayableFallible::Correct(PendingPayable { + recipient_wallet: make_wallet("bcd"), + hash: make_tx_hash(890), + })]), + response_skeleton_opt: None, + }; + let blockchain_bridge_counter_msg_setup_for_pending_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( + RequestTransactionReceipts, + expected_report_transaction_receipts.clone(), + &subject_addr + ); + let blockchain_bridge_counter_msg_setup_for_payable_scanner = setup_for_counter_msg_triggered_via_type_id!( + QualifiedPayablesMessage, + expected_sent_payables.clone(), + &subject_addr + ); + send_bind_message!(subject_subs, peer_actors); + addresses + .blockchain_bridge_addr + .try_send(SetUpCounterMsgs::new(vec![ + blockchain_bridge_counter_msg_setup_for_pending_payable_scanner, + blockchain_bridge_counter_msg_setup_for_payable_scanner, + ])) + .unwrap(); + + send_start_message!(subject_subs); + + // The system is stopped by the NotifyHandleLaterMock for the PendingPayable scanner + system.run(); + assert_pending_payable_scanner_for_some_pending_payable_found( + test_name, + consuming_wallet.clone(), + pending_payable_start_scan_params_arc, + pending_payable_finish_scan_params_arc, + scan_for_pending_payables_notify_later_params_arc, + pending_payable_expected_notify_later_interval, + expected_report_transaction_receipts, + ); + assert_payable_scanner_for_some_pending_payable_found( + test_name, + consuming_wallet, + expected_sent_payables, + payable_finish_scan_params_arc, + payable_start_scan_params_arc, + scan_for_new_payables_notify_later_params_arc, + scan_for_new_payables_notify_params_arc, + scan_for_retry_payables_notify_params_arc, + ); + assert_receivable_scanner( + test_name, + earning_wallet, + receivable_start_scan_params_arc, + receivable_scan_interval, + scan_for_receivables_notify_later_params_arc, + ); + // Given the assertions proving that the pending payable scanner will run multiple times + // before the new payable scanner runs at least once (even not scheduled yet), its front + // position is clear + } + + fn set_up_subject_for_no_pending_payables_found_startup_test( + test_name: &str, + scan_for_pending_payables_notify_later_params_arc: &Arc< + Mutex>, + >, + scan_for_new_payables_notify_later_params_arc: &Arc< + Mutex>, + >, + scan_for_new_payables_notify_params_arc: &Arc>>, + scan_for_retry_payables_notify_params_arc: &Arc>>, + compute_interval_params_arc: &Arc>>, + scan_for_receivables_notify_later_params_arc: &Arc< + Mutex>, + >, + config: BootstrapperConfig, + pending_payable_scanner: ScannerMock< + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, + receivable_scanner: ScannerMock< + RetrieveTransactions, + ReceivedPayments, + Option, + >, + payable_scanner: ScannerMock, + ) -> (Accountant, Duration, Duration) { let mut subject = AccountantBuilder::default() .logger(Logger::new(test_name)) .bootstrapper_config(config) @@ -2627,10 +2844,10 @@ mod tests { .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( payable_scanner, ))); - subject.scan_schedulers.pending_payable.handle = Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(&scan_for_pending_payables_notify_later_params_arc), - ); + let pending_payable_notify_later_handle_mock = NotifyLaterHandleMock::default() + .notify_later_params(&scan_for_pending_payables_notify_later_params_arc); + subject.scan_schedulers.pending_payable.handle = + Box::new(pending_payable_notify_later_handle_mock); let new_payable_expected_computed_interval = Duration::from_secs(3600); subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default() @@ -2640,32 +2857,183 @@ mod tests { NotifyHandleMock::default().notify_params(&scan_for_retry_payables_notify_params_arc), ); subject.scan_schedulers.payable.new_payable_notify = Box::new( - NotifyHandleMock::default() - .capture_msg_and_let_it_fly_on() - .notify_params(&scan_for_new_payables_notify_params_arc), - ); - subject.scan_schedulers.receivable.handle = Box::new( - NotifyLaterHandleMock::default() - .notify_later_params(&scan_for_receivables_notify_later_params_arc) - .stop_system_on_count_received(1), + NotifyHandleMock::default().notify_params(&scan_for_new_payables_notify_params_arc), ); - // Important that this is short because the test relies on it to stop the system. + let receivable_notify_later_handle_mock = NotifyLaterHandleMock::default() + .notify_later_params(&scan_for_receivables_notify_later_params_arc) + .stop_system_on_count_received(1); + subject.scan_schedulers.receivable.handle = Box::new(receivable_notify_later_handle_mock); + // Important that this is made short because the test relies on it with the system stop. let receivable_scan_interval = Duration::from_millis(50); subject.scan_schedulers.receivable.interval = receivable_scan_interval; let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() .compute_interval_params(&compute_interval_params_arc) .compute_interval_result(Some(new_payable_expected_computed_interval)); subject.scan_schedulers.payable.dyn_interval_computer = Box::new(dyn_interval_computer); - let peer_actors = peer_actors_builder().build(); - let subject_addr: Addr = subject.start(); - let subject_subs = Accountant::make_subs_from(&subject_addr); - send_bind_message!(subject_subs, peer_actors); + ( + subject, + new_payable_expected_computed_interval, + receivable_scan_interval, + ) + } - send_start_message!(subject_subs); + fn set_up_subject_for_some_pending_payable_found_startup_test( + test_name: &str, + scan_for_pending_payables_notify_later_params_arc: &Arc< + Mutex>, + >, + scan_for_new_payables_notify_later_params_arc: &Arc< + Mutex>, + >, + scan_for_new_payables_notify_params_arc: &Arc>>, + scan_for_retry_payables_notify_params_arc: &Arc>>, + scan_for_receivables_notify_later_params_arc: &Arc< + Mutex>, + >, + config: BootstrapperConfig, + pending_payable_scanner: ScannerMock< + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, + receivable_scanner: ScannerMock< + RetrieveTransactions, + ReceivedPayments, + Option, + >, + payable_scanner: ScannerMock, + ) -> (Accountant, Duration, Duration) { + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .bootstrapper_config(config) + .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Mock( + receivable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); + let pending_payable_notify_later_handle_mock = NotifyLaterHandleMock::default() + .notify_later_params(&scan_for_pending_payables_notify_later_params_arc) + // This should stop the system + .stop_system_on_count_received(1); + subject.scan_schedulers.pending_payable.handle = + Box::new(pending_payable_notify_later_handle_mock); + let pending_payable_scan_interval = Duration::from_secs(3600); + subject.scan_schedulers.pending_payable.interval = pending_payable_scan_interval; + subject.scan_schedulers.payable.new_payable_notify_later = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&scan_for_new_payables_notify_later_params_arc), + ); + subject.scan_schedulers.payable.retry_payable_notify = Box::new( + NotifyHandleMock::default() + .notify_params(&scan_for_retry_payables_notify_params_arc) + .capture_msg_and_let_it_fly_on(), + ); + subject.scan_schedulers.payable.new_payable_notify = Box::new( + NotifyHandleMock::default().notify_params(&scan_for_new_payables_notify_params_arc), + ); + let receivable_notify_later_handle_mock = NotifyLaterHandleMock::default() + .notify_later_params(&scan_for_receivables_notify_later_params_arc); + let receivable_scan_interval = Duration::from_secs(3600); + subject.scan_schedulers.receivable.interval = receivable_scan_interval; + subject.scan_schedulers.receivable.handle = Box::new(receivable_notify_later_handle_mock); + ( + subject, + pending_payable_scan_interval, + receivable_scan_interval, + ) + } - // The system is stopped by the NotifyLaterHandleMock for the Receivable scanner - system.run(); - // Assertions on the pending payable scanner proper functioning + fn assert_pending_payable_scanner_for_no_pending_payable_found( + test_name: &str, + consuming_wallet: Wallet, + pending_payable_start_scan_params_arc: Arc< + Mutex, Logger, String)>>, + >, + scan_for_pending_payables_notify_later_params_arc: Arc< + Mutex>, + >, + ) -> SystemTime { + let (pp_scan_started_at, pp_logger) = + pending_payable_common(consuming_wallet, pending_payable_start_scan_params_arc); + let scan_for_pending_payables_notify_later_params = + scan_for_pending_payables_notify_later_params_arc + .lock() + .unwrap(); + // The part of running the `NewPayableScanner` is deliberately omitted here, we stop + // the test right before that. Normally, the first occasion for scheduling + // the `PendingPayableScanner` would've lied no sooner than after the `NewPayableScan` + // finishes, having produced at least one blockchain transactions. + assert!( + scan_for_pending_payables_notify_later_params.is_empty(), + "We did not expect to see another schedule for pending payables, but it happened {:?}", + scan_for_pending_payables_notify_later_params + ); + assert_using_the_same_logger(&pp_logger, test_name, Some("pp")); + pp_scan_started_at + } + + fn assert_pending_payable_scanner_for_some_pending_payable_found( + test_name: &str, + consuming_wallet: Wallet, + pending_payable_start_scan_params_arc: Arc< + Mutex, Logger, String)>>, + >, + pending_payable_finish_scan_params_arc: Arc< + Mutex>, + >, + scan_for_pending_payables_notify_later_params_arc: Arc< + Mutex>, + >, + pending_payable_expected_notify_later_interval: Duration, + expected_report_tx_receipts_msg: ReportTransactionReceipts, + ) { + let (_, pp_start_scan_logger) = + pending_payable_common(consuming_wallet, pending_payable_start_scan_params_arc); + assert_using_the_same_logger(&pp_start_scan_logger, test_name, Some("pp start scan")); + let mut pending_payable_finish_scan_params = + pending_payable_finish_scan_params_arc.lock().unwrap(); + let (actual_report_tx_receipts_msg, pp_finish_scan_logger) = + pending_payable_finish_scan_params.remove(0); + assert_eq!( + actual_report_tx_receipts_msg, + expected_report_tx_receipts_msg + ); + assert_using_the_same_logger(&pp_finish_scan_logger, test_name, Some("pp finish scan")); + let scan_for_pending_payables_notify_later_params = + scan_for_pending_payables_notify_later_params_arc + .lock() + .unwrap(); + // This is the moment when the test ends. It says that we went the way of the pending payable + // sequence, instead of calling the NewPayableScan just after the initial pending payable + // scan. + assert_eq!( + *scan_for_pending_payables_notify_later_params, + vec![( + ScanForPendingPayables { + response_skeleton_opt: None + }, + pending_payable_expected_notify_later_interval + )], + ); + } + + fn pending_payable_common( + consuming_wallet: Wallet, + pending_payable_start_scan_params_arc: Arc< + Mutex, Logger, String)>>, + >, + ) -> (SystemTime, Logger) { let mut pending_payable_params = pending_payable_start_scan_params_arc.lock().unwrap(); let ( pp_wallet, @@ -2686,21 +3054,18 @@ mod tests { "Should be empty but was {:?}", pending_payable_params ); - let scan_for_pending_payables_notify_later_params = - scan_for_pending_payables_notify_later_params_arc - .lock() - .unwrap(); - // The part of running the `NewPayableScanner` is deliberately omitted here, we stop - // the test right before that. Normally, the first occasion for scheduling - // the `PendingPayableScanner` would've lied no sooner than after the `NewPayableScan` - // finishes, having produced at least one blockchain transactions. - assert!( - scan_for_pending_payables_notify_later_params.is_empty(), - "We did not expect to see another schedule for pending payables, but it happened {:?}", - scan_for_pending_payables_notify_later_params - ); - assert_using_the_same_logger(&pp_logger, test_name, Some("pp")); - // Assertions on the payable scanner proper functioning + (pp_scan_started_at, pp_logger) + } + + fn assert_payable_scanner_for_no_pending_payable_found( + scan_for_new_payables_notify_later_params_arc: Arc< + Mutex>, + >, + new_payable_expected_computed_interval: Duration, + scan_for_new_payables_notify_params_arc: Arc>>, + scan_for_retry_payables_notify_params_arc: Arc>>, + compute_interval_params_arc: Arc>>, + ) -> SystemTime { // First, there is no functionality from the payable scanner actually running. // We only witness it to be scheduled. let scan_for_new_payables_notify_later_params = @@ -2734,7 +3099,90 @@ mod tests { "We did not expect any scheduling of retry payables, but it happened {:?}", scan_for_retry_payables_notify_params ); - // Assertions on the receivable scanner proper functioning + p_scheduling_now + } + + fn assert_payable_scanner_for_some_pending_payable_found( + test_name: &str, + consuming_wallet: Wallet, + expected_sent_payables: SentPayables, + payable_finish_scan_params_arc: Arc>>, + payable_start_scan_params_arc: Arc< + Mutex, Logger, String)>>, + >, + scan_for_new_payables_notify_later_params_arc: Arc< + Mutex>, + >, + scan_for_new_payables_notify_params_arc: Arc>>, + scan_for_retry_payables_notify_params_arc: Arc>>, + ) { + let mut payable_start_scan_params = payable_start_scan_params_arc.lock().unwrap(); + let (p_wallet, _, p_response_skeleton_opt, p_start_scan_logger, p_trigger_msg_type_str) = + payable_start_scan_params.remove(0); + assert_eq!(p_wallet, consuming_wallet); + assert_eq!(p_response_skeleton_opt, None); + // Important: it's the proof that we're dealing with the RetryPayableScanner not NewPayableScanner + assert!( + p_trigger_msg_type_str.contains("RetryPayable"), + "Should contain RetryPayable but {}", + p_trigger_msg_type_str + ); + assert!( + payable_start_scan_params.is_empty(), + "Should be empty but was {:?}", + payable_start_scan_params + ); + assert_using_the_same_logger(&p_start_scan_logger, test_name, Some("retry payable start")); + let mut payable_finish_scan_params = payable_finish_scan_params_arc.lock().unwrap(); + let (actual_sent_payable, p_finish_scan_logger) = payable_finish_scan_params.remove(0); + assert_eq!(actual_sent_payable, expected_sent_payables,); + assert!( + payable_finish_scan_params.is_empty(), + "Should be empty but was {:?}", + payable_finish_scan_params + ); + assert_using_the_same_logger( + &p_finish_scan_logger, + test_name, + Some("retry payable finish"), + ); + let scan_for_new_payables_notify_later_params = + scan_for_new_payables_notify_later_params_arc + .lock() + .unwrap(); + assert!( + scan_for_new_payables_notify_later_params.is_empty(), + "We did not expect any later scheduling of new payables, but it happened {:?}", + scan_for_new_payables_notify_later_params + ); + let scan_for_new_payables_notify_params = + scan_for_new_payables_notify_params_arc.lock().unwrap(); + assert!( + scan_for_new_payables_notify_params.is_empty(), + "We did not expect any immediate scheduling of new payables, but it happened {:?}", + scan_for_new_payables_notify_params + ); + let scan_for_retry_payables_notify_params = + scan_for_retry_payables_notify_params_arc.lock().unwrap(); + assert_eq!( + *scan_for_retry_payables_notify_params, + vec![ScanForRetryPayables { + response_skeleton_opt: None + }], + ); + } + + fn assert_receivable_scanner( + test_name: &str, + earning_wallet: Wallet, + receivable_start_scan_params_arc: Arc< + Mutex, Logger, String)>>, + >, + receivable_scan_interval: Duration, + scan_for_receivables_notify_later_params_arc: Arc< + Mutex>, + >, + ) { let mut receivable_start_scan_params = receivable_start_scan_params_arc.lock().unwrap(); let (r_wallet, _r_started_at, r_response_skeleton_opt, r_logger, r_trigger_msg_name_str) = receivable_start_scan_params.remove(0); @@ -2762,11 +3210,6 @@ mod tests { receivable_scan_interval )] ); - // Finally, an assertion to prove course of actions in time. We expect the pending payable - // scanner to take place before the payable scanner. - // I do believe it's impossible that these two instants would happen at the same time, until - // this is run on a real super-giga-computer. - assert!(pp_scan_started_at < p_scheduling_now); } #[test] @@ -2998,7 +3441,7 @@ mod tests { }; let blockchain_bridge_addr = blockchain_bridge.start(); let request_transaction_receipts = RequestTransactionReceipts { - pending_payable: vec![pending_payable_fingerprint], + pending_payable_fingerprints: vec![pending_payable_fingerprint], response_skeleton_opt: None, }; let pending_payable_scanner = ScannerMock::new() @@ -3531,7 +3974,7 @@ mod tests { assert_eq!( received_msg, &RequestTransactionReceipts { - pending_payable: vec![payable_fingerprint_1, payable_fingerprint_2], + pending_payable_fingerprints: vec![payable_fingerprint_1, payable_fingerprint_2], response_skeleton_opt: None, } ); diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 7cc4f42e4..88d186849 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -885,7 +885,7 @@ impl StartableScanner filtered_pending_payable.len() ); Ok(RequestTransactionReceipts { - pending_payable: filtered_pending_payable, + pending_payable_fingerprints: filtered_pending_payable, response_skeleton_opt, }) } @@ -3123,7 +3123,7 @@ mod tests { assert_eq!( result, Ok(RequestTransactionReceipts { - pending_payable: fingerprints, + pending_payable_fingerprints: fingerprints, response_skeleton_opt: None }) ); diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 5bde2ac7d..e0a52683f 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -18,10 +18,9 @@ use masq_lib::logger::{Logger, TIME_FORMATTING_STRING}; use masq_lib::ui_gateway::NodeToUiMessage; use regex::Regex; use std::cell::RefCell; -use std::panic::panic_any; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use time::{format_description, OffsetDateTime, PrimitiveDateTime}; +use time::{format_description, PrimitiveDateTime}; #[derive(Default)] pub struct NewPayableScanDynIntervalComputerMock { diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 5b2fcccaa..b93d8770c 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -430,7 +430,7 @@ impl BlockchainBridge { .expect("Accountant is unbound"); let transaction_hashes = msg - .pending_payable + .pending_payable_fingerprints .iter() .map(|finger_print| finger_print.hash) .collect::>(); @@ -443,7 +443,7 @@ impl BlockchainBridge { let pairs = transaction_receipts_results .into_iter() - .zip(msg.pending_payable.into_iter()) + .zip(msg.pending_payable_fingerprints.into_iter()) .collect_vec(); accountant_recipient @@ -1179,7 +1179,7 @@ mod tests { let peer_actors = peer_actors_builder().accountant(accountant).build(); send_bind_message!(subject_subs, peer_actors); let msg = RequestTransactionReceipts { - pending_payable: vec![ + pending_payable_fingerprints: vec![ pending_payable_fingerprint_1.clone(), pending_payable_fingerprint_2.clone(), ], @@ -1360,7 +1360,7 @@ mod tests { .report_transaction_receipts_sub_opt = Some(report_transaction_receipt_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable: vec![ + pending_payable_fingerprints: vec![ fingerprint_1.clone(), fingerprint_2.clone(), fingerprint_3.clone(), @@ -1439,7 +1439,7 @@ mod tests { .report_transaction_receipts_sub_opt = Some(report_transaction_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable: vec![fingerprint_1, fingerprint_2], + pending_payable_fingerprints: vec![fingerprint_1, fingerprint_2], response_skeleton_opt: None, }; let system = System::new("test"); diff --git a/node/src/test_utils/recorder_counter_msgs.rs b/node/src/test_utils/recorder_counter_msgs.rs index 252431224..cfda62396 100644 --- a/node/src/test_utils/recorder_counter_msgs.rs +++ b/node/src/test_utils/recorder_counter_msgs.rs @@ -110,6 +110,8 @@ impl CounterMessages { } } +// Note that you're not limited to triggering an only message a time, but you can supply more +// messages to this macro, all triggered by the same type id. #[macro_export] macro_rules! setup_for_counter_msg_triggered_via_type_id{ ($trigger_msg_type: ty, $($owned_counter_msg: expr, $respondent_actor_addr_ref: expr),+) => { From e09a1ef5f358ca82a7b502bf18858bd44e5e28dd Mon Sep 17 00:00:00 2001 From: Bert Date: Thu, 5 Jun 2025 23:31:02 +0200 Subject: [PATCH 39/49] GH-602: huge tests refactored --- node/src/accountant/mod.rs | 208 ++++++++++-------- node/src/accountant/scanners/mod.rs | 5 +- .../accountant/scanners/scan_schedulers.rs | 137 +++++++----- 3 files changed, 194 insertions(+), 156 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index f93a7ee96..3e36a704d 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -76,7 +76,7 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; -use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, SchedulingAfterHaltedScan, ScanSchedulers}; +use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, StartScanErrorResponse, ScanSchedulers}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; @@ -225,7 +225,7 @@ impl Handler for Accountant { // of the RetryPayableScanner, which finishes, and the PendingPayablesScanner is scheduled // to run again. However, not from here. let response_skeleton_opt = msg.response_skeleton_opt; - if let SchedulingAfterHaltedScan::Schedule(ScanType::Payables) = + if let StartScanErrorResponse::Schedule(ScanType::Payables) = self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) { self.scan_schedulers @@ -245,7 +245,7 @@ impl Handler for Accountant { // the PendingPayableScanner whose job is to evaluate if it has seen every pending payable // complete. That's the moment when another run of the NewPayableScanner makes sense again. let response_skeleton = msg.response_skeleton_opt; - if let SchedulingAfterHaltedScan::Schedule(ScanType::Payables) = + if let StartScanErrorResponse::Schedule(ScanType::Payables) = self.handle_request_of_scan_for_new_payable(response_skeleton) { self.scan_schedulers @@ -893,7 +893,7 @@ impl Accountant { fn handle_request_of_scan_for_new_payable( &mut self, response_skeleton_opt: Option, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( @@ -913,7 +913,7 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - SchedulingAfterHaltedScan::DoNotSchedule + StartScanErrorResponse::DoNotSchedule } Err(e) => self.handle_start_scan_error_and_prevent_scan_stall_point( PayableSequenceScanner::NewPayables, @@ -926,7 +926,7 @@ impl Accountant { fn handle_request_of_scan_for_retry_payable( &mut self, response_skeleton_opt: Option, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_retry_payable_scan_guarded( @@ -945,7 +945,7 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - SchedulingAfterHaltedScan::DoNotSchedule + StartScanErrorResponse::DoNotSchedule } Err(e) => self.handle_start_scan_error_and_prevent_scan_stall_point( PayableSequenceScanner::RetryPayables, @@ -958,7 +958,7 @@ impl Accountant { fn handle_request_of_scan_for_pending_payable( &mut self, response_skeleton_opt: Option, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_pending_payable_scan_guarded( @@ -971,14 +971,14 @@ impl Accountant { None => Err(StartScanError::NoConsumingWalletFound), }; - let hint: SchedulingAfterHaltedScan = match result { + let hint: StartScanErrorResponse = match result { Ok(scan_message) => { self.request_transaction_receipts_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - SchedulingAfterHaltedScan::DoNotSchedule + StartScanErrorResponse::DoNotSchedule } Err(e) => { let initial_pending_payable_scan = self.scanners.initial_pending_payable_scan(); @@ -1004,7 +1004,7 @@ impl Accountant { scanner: PayableSequenceScanner, e: StartScanError, response_skeleton_opt: Option, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { let is_externally_triggered = response_skeleton_opt.is_some(); e.log_error(&self.logger, scanner.into(), is_externally_triggered); @@ -3231,7 +3231,7 @@ mod tests { System::current().stop(); system.run(); let flag_after = subject.scanners.initial_pending_payable_scan(); - assert_eq!(hint, SchedulingAfterHaltedScan::DoNotSchedule); + assert_eq!(hint, StartScanErrorResponse::DoNotSchedule); assert_eq!(flag_before, true); assert_eq!(flag_after, false); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); @@ -3251,10 +3251,7 @@ mod tests { let hint = subject.handle_request_of_scan_for_pending_payable(None); let flag_after = subject.scanners.initial_pending_payable_scan(); - assert_eq!( - hint, - SchedulingAfterHaltedScan::Schedule(ScanType::Payables) - ); + assert_eq!(hint, StartScanErrorResponse::Schedule(ScanType::Payables)); assert_eq!(flag_before, true); assert_eq!(flag_after, false); } @@ -3262,7 +3259,7 @@ mod tests { #[test] #[should_panic( expected = "internal error: entered unreachable code: ScanAlreadyRunning { \ - pertinent_scanner: PendingPayables, started_at: SystemTime { tv_sec: 0, tv_nsec: 0 } } \ + cross_scan_cause_opt: None, started_at: SystemTime { tv_sec: 0, tv_nsec: 0 } } \ should be impossible with PendingPayableScanner in automatic mode" )] fn initial_pending_payable_scan_hits_unexpected_error() { @@ -3400,20 +3397,20 @@ mod tests { #[test] fn periodical_scanning_for_payables_works() { init_test_logging(); - let test_name = "periodical_scanning_for_payable_works"; + let test_name = "periodical_scanning_for_payables_works"; let start_scan_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); let start_scan_payable_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); let notify_payable_params_arc = Arc::new(Mutex::new(vec![])); let system = System::new(test_name); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge_addr = blockchain_bridge.start(); let payable_account = make_payable_account(123); let qualified_payable = vec![payable_account.clone()]; let consuming_wallet = make_paying_wallet(b"consuming"); - let agent = BlockchainAgentMock::default(); let counter_msg_1 = BlockchainAgentWithContextMessage { qualified_payables: qualified_payable.clone(), - agent: Box::new(agent), + agent: Box::new(BlockchainAgentMock::default()), response_skeleton_opt: None, }; let transaction_hash = make_tx_hash(789); @@ -3439,73 +3436,26 @@ mod tests { )], response_skeleton_opt: None, }; - let blockchain_bridge_addr = blockchain_bridge.start(); - let request_transaction_receipts = RequestTransactionReceipts { + let request_transaction_receipts_msg = RequestTransactionReceipts { pending_payable_fingerprints: vec![pending_payable_fingerprint], response_skeleton_opt: None, }; - let pending_payable_scanner = ScannerMock::new() - .scan_started_at_result(None) - .start_scan_params(&start_scan_pending_payable_params_arc) - .start_scan_result(Ok(request_transaction_receipts.clone())) - .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); let qualified_payables_msg = QualifiedPayablesMessage { qualified_payables: qualified_payable.clone(), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: None, }; - let payable_scanner = ScannerMock::new() - .scan_started_at_result(None) - // Always checking also on the payable scanner when handling ScanForPendingPayable - .scan_started_at_result(None) - .start_scan_params(&start_scan_payable_params_arc) - .start_scan_result(Ok(qualified_payables_msg.clone())) - .finish_scan_result(PayableScanResult { - ui_response_opt: None, - result: OperationOutcome::NewPendingPayable, - }); - let mut config = bc_from_earning_wallet(make_wallet("hi")); - config.scan_intervals_opt = Some(ScanIntervals { - // This simply means that we're gonna surplus this value (it abides by how many pending - // payable cycles have to go in between before the lastly submitted txs are confirmed), - payable_scan_interval: Duration::from_millis(10), - pending_payable_scan_interval: Duration::from_millis(50), - receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner - }); - let mut subject = AccountantBuilder::default() - .bootstrapper_config(config) - .consuming_wallet(consuming_wallet.clone()) - .logger(Logger::new(test_name)) - .build(); - subject - .scanners - .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( - pending_payable_scanner, - ))); - subject - .scanners - .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( - payable_scanner, - ))); - subject - .scanners - .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); //skipping - subject.scan_schedulers.pending_payable.handle = Box::new( - NotifyLaterHandleMock::::default() - .notify_later_params(¬ify_later_pending_payables_params_arc) - .capture_msg_and_let_it_fly_on(), - ); - subject.scan_schedulers.payable.new_payable_notify = Box::new( - NotifyHandleMock::::default() - .notify_params(¬ify_payable_params_arc) - // This should stop the system. If anything goes wrong, the SystemKillerActor will. - .stop_system_on_count_received(1), + let subject = set_up_subject_to_prove_periodical_payable_scan( + test_name, + &blockchain_bridge_addr, + &consuming_wallet, + &qualified_payables_msg, + &request_transaction_receipts_msg, + &start_scan_pending_payable_params_arc, + &start_scan_payable_params_arc, + ¬ify_later_pending_payables_params_arc, + ¬ify_payable_params_arc, ); - subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.clone().recipient()); - subject.outbound_payments_instructions_sub_opt = - Some(blockchain_bridge_addr.clone().recipient()); - subject.request_transaction_receipts_sub_opt = - Some(blockchain_bridge_addr.clone().recipient()); let subject_addr = subject.start(); let set_up_counter_msgs = SetUpCounterMsgs::new(vec![ setup_for_counter_msg_triggered_via_type_id!( @@ -3537,7 +3487,6 @@ mod tests { let time_before = SystemTime::now(); system.run(); let time_after = SystemTime::now(); - let tlh = TestLogHandler::new(); let mut start_scan_payable_params = start_scan_payable_params_arc.lock().unwrap(); let (wallet, timestamp, response_skeleton_opt, logger, _) = start_scan_payable_params.remove(0); @@ -3545,7 +3494,7 @@ mod tests { assert!(time_before <= timestamp && timestamp <= time_after); assert_eq!(response_skeleton_opt, None); assert!(start_scan_payable_params.is_empty()); - assert_using_the_same_logger(&logger, test_name, Some("abc")); + assert_using_the_same_logger(&logger, test_name, Some("start scan payable")); let mut start_scan_pending_payable_params = start_scan_pending_payable_params_arc.lock().unwrap(); let (wallet, timestamp, response_skeleton_opt, logger, _) = @@ -3554,7 +3503,7 @@ mod tests { assert!(time_before <= timestamp && timestamp <= time_after); assert_eq!(response_skeleton_opt, None); assert!(start_scan_pending_payable_params.is_empty()); - assert_using_the_same_logger(&logger, test_name, Some("def")); + assert_using_the_same_logger(&logger, test_name, Some("start scan pending payable")); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); let actual_qualified_payables_msg = blockchain_bridge_recording.get_record::(0); @@ -3567,7 +3516,10 @@ mod tests { ); let actual_requested_receipts_1 = blockchain_bridge_recording.get_record::(2); - assert_eq!(actual_requested_receipts_1, &request_transaction_receipts); + assert_eq!( + actual_requested_receipts_1, + &request_transaction_receipts_msg + ); let notify_later_pending_payables_params = notify_later_pending_payables_params_arc.lock().unwrap(); assert_eq!( @@ -3586,12 +3538,83 @@ mod tests { response_skeleton_opt: None },] ); - tlh.exists_log_containing(&format!( - "DEBUG: {test_name}: verifying payable scanner logger" - )); - tlh.exists_log_containing(&format!( - "DEBUG: {test_name}: verifying pending payable scanner logger" - )); + } + + fn set_up_subject_to_prove_periodical_payable_scan( + test_name: &str, + blockchain_bridge_addr: &Addr, + consuming_wallet: &Wallet, + qualified_payables_msg: &QualifiedPayablesMessage, + request_transaction_receipts: &RequestTransactionReceipts, + start_scan_pending_payable_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + start_scan_payable_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + notify_later_pending_payables_params_arc: &Arc< + Mutex>, + >, + notify_payable_params_arc: &Arc>>, + ) -> Accountant { + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&start_scan_pending_payable_params_arc) + .start_scan_result(Ok(request_transaction_receipts.clone())) + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); + let payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + // Always checking also on the payable scanner when handling ScanForPendingPayable + .scan_started_at_result(None) + .start_scan_params(&start_scan_payable_params_arc) + .start_scan_result(Ok(qualified_payables_msg.clone())) + .finish_scan_result(PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::NewPendingPayable, + }); + let mut config = bc_from_earning_wallet(make_wallet("hi")); + config.scan_intervals_opt = Some(ScanIntervals { + // This simply means that we're gonna surplus this value (it abides by how many pending + // payable cycles have to go in between before the lastly submitted txs are confirmed), + payable_scan_interval: Duration::from_millis(10), + pending_payable_scan_interval: Duration::from_millis(50), + receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner + }); + let mut subject = AccountantBuilder::default() + .bootstrapper_config(config) + .consuming_wallet(consuming_wallet.clone()) + .logger(Logger::new(test_name)) + .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); //skipping + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::::default() + .notify_later_params(¬ify_later_pending_payables_params_arc) + .capture_msg_and_let_it_fly_on(), + ); + subject.scan_schedulers.payable.new_payable_notify = Box::new( + NotifyHandleMock::::default() + .notify_params(¬ify_payable_params_arc) + // This should stop the system. If anything goes wrong, the SystemKillerActor will. + .stop_system_on_count_received(1), + ); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.clone().recipient()); + subject.outbound_payments_instructions_sub_opt = + Some(blockchain_bridge_addr.clone().recipient()); + subject.request_transaction_receipts_sub_opt = + Some(blockchain_bridge_addr.clone().recipient()); + subject } #[test] @@ -3610,7 +3633,7 @@ mod tests { .is_some(); assert_eq!(has_scan_started, false); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: Cannot initiate Payables scan because no consuming wallet was found." + "WARN: {test_name}: Cannot initiate Payables scan because no consuming wallet was found." )); } @@ -3630,7 +3653,7 @@ mod tests { .is_some(); assert_eq!(has_scan_started, false); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: Cannot initiate PendingPayables scan because no consuming wallet was found." + "WARN: {test_name}: Cannot initiate PendingPayables scan because no consuming wallet was found." )); } @@ -3749,10 +3772,7 @@ mod tests { System::current().stop(); system.run(); - assert_eq!( - result, - SchedulingAfterHaltedScan::Schedule(ScanType::Payables) - ); + assert_eq!(result, StartScanErrorResponse::Schedule(ScanType::Payables)); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recordings.len(), 0); } diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 88d186849..58c49002b 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -904,10 +904,7 @@ impl Scanner for PendingPay let requires_payment_retry = match message.fingerprints_with_receipts.is_empty() { true => { warning!(logger, "No transaction receipts found."); - todo!( - "This requires the payment retry. It must be processed by using some of the new \ - methods on the SentPaybleDAO. After GH-631 is done." - ); + todo!("This requires the payment retry. GH-631 must be completed first"); } false => { debug!( diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index e0724cf5f..abdc9bffa 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -44,7 +44,7 @@ pub enum PayableScanSchedulerError { } #[derive(Debug, PartialEq)] -pub enum SchedulingAfterHaltedScan { +pub enum StartScanErrorResponse { Schedule(ScanType), DoNotSchedule, } @@ -122,9 +122,9 @@ impl PayableScanScheduler { } } - // This message is always inserted into the Accountant's mailbox with no delay. - // Can also be triggered by command, following up after the PendingPayableScanner that - // requests it. That's why the response skeleton is possible to be used. + // This message ships into the Accountant's mailbox with no delay. + // Can also be triggered by command, following up after the PendingPayableScanner + // that requests it. That's why the response skeleton is possible to be used. pub fn schedule_retry_payable_scan( &self, ctx: &mut Context, @@ -221,19 +221,20 @@ where } } -// Scanners that conclude by scheduling a later scan (usually different from this one) must handle -// StartScanErrors carefully to maintain continuity and periodicity. Poor handling could disrupt -// the entire scan chain. Where possible, a different type of scan may be scheduled (avoiding -// repetition of the erroneous scan) to prevent a full panic, while ensuring no unresolved issues -// are left for future scans. A panic is justified only if the error is deemed impossible by design -// within the broader context of that location. +// Scanners that take part in a scan sequence composed of different scanners must handle +// StartScanErrors delicately to maintain the continuity and periodicity of this process. Where +// possible, either the same, some other, but traditional, or even a totally unrelated scan chosen +// just in the event of emergency, may be scheduled. The intention is to prevent a full panic while +// ensuring no harmful, toxic issues are left behind for the future scans. Following that philosophy, +// panic is justified only if the error was thought to be impossible by design and contextual +// things but still happened. pub trait RescheduleScanOnErrorResolver { fn resolve_rescheduling_for_given_error( &self, scanner: PayableSequenceScanner, error: &StartScanError, is_externally_triggered: bool, - ) -> SchedulingAfterHaltedScan; + ) -> StartScanErrorResponse; } #[derive(Default)] @@ -245,7 +246,7 @@ impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverReal { scanner: PayableSequenceScanner, error: &StartScanError, is_externally_triggered: bool, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { match scanner { PayableSequenceScanner::NewPayables => { Self::resolve_new_payables(error, is_externally_triggered) @@ -268,28 +269,28 @@ impl RescheduleScanOnErrorResolverReal { fn resolve_new_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { if is_externally_triggered { - SchedulingAfterHaltedScan::DoNotSchedule + StartScanErrorResponse::DoNotSchedule } else if matches!(err, StartScanError::ScanAlreadyRunning { .. }) { unreachable!( "an automatic scan of NewPayableScanner should never interfere with itself {:?}", err ) } else { - SchedulingAfterHaltedScan::Schedule(ScanType::Payables) + StartScanErrorResponse::Schedule(ScanType::Payables) } } - // This looks paradoxical, but this scanner is meant to be shielded by the scanner right before + // Paradoxical at first, but this scanner is meant to be shielded by the scanner right before // it. That should ensure this scanner will not be requested if there was already something - // fishy. We can go strict. + // fishy. We can impose strictness. fn resolve_retry_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { if is_externally_triggered { - SchedulingAfterHaltedScan::DoNotSchedule + StartScanErrorResponse::DoNotSchedule } else { unreachable!( "{:?} should be impossible with RetryPayableScanner in automatic mode", @@ -302,12 +303,12 @@ impl RescheduleScanOnErrorResolverReal { err: &StartScanError, initial_pending_payable_scan: bool, is_externally_triggered: bool, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { if is_externally_triggered { - SchedulingAfterHaltedScan::DoNotSchedule + StartScanErrorResponse::DoNotSchedule } else if err == &StartScanError::NothingToProcess { if initial_pending_payable_scan { - SchedulingAfterHaltedScan::Schedule(ScanType::Payables) + StartScanErrorResponse::Schedule(ScanType::Payables) } else { unreachable!( "the automatic pending payable scan should always be requested only in need, \ @@ -315,7 +316,21 @@ impl RescheduleScanOnErrorResolverReal { ) } } else if err == &StartScanError::NoConsumingWalletFound { - SchedulingAfterHaltedScan::Schedule(ScanType::Payables) + if initial_pending_payable_scan { + // Cannot deduce there are strayed pending payables from the previous Node's run + // (StartScanError::NoConsumingWalletFound is thrown before + // StartScanError::NothingToProcess can be evaluated); but may be cautious and + // prevent starting the NewPayableScanner. Repeating this scan endlessly may alarm + // the user. + // TODO Correctly, a check-point during the bootstrap should be the solution, + // which wouldn't allow to come this far. + StartScanErrorResponse::Schedule(ScanType::PendingPayables) + } else { + unreachable!( + "PendingPayableScanner called later than the initial attempt, but \ + the consuming wallet is still missing; this should not be possible" + ) + } } else { unreachable!( "{:?} should be impossible with PendingPayableScanner in automatic mode", @@ -329,7 +344,7 @@ impl RescheduleScanOnErrorResolverReal { mod tests { use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, - PayableSequenceScanner, ScanSchedulers, SchedulingAfterHaltedScan, + PayableSequenceScanner, ScanSchedulers, StartScanErrorResponse, }; use crate::accountant::scanners::{MTError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; @@ -572,7 +587,7 @@ mod tests { assert_eq!( result, - SchedulingAfterHaltedScan::DoNotSchedule, + StartScanErrorResponse::DoNotSchedule, "We expected DoNotSchedule but got {:?} at idx {} for {:?}", result, idx, @@ -598,7 +613,7 @@ mod tests { assert_eq!( result, - SchedulingAfterHaltedScan::Schedule(ScanType::Payables), + StartScanErrorResponse::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got {:?}", result, ); @@ -626,42 +641,48 @@ mod tests { } #[test] - fn resolve_error_for_pending_payables_if_no_consuming_wallet_found() { - fn test_no_consuming_wallet_found( - subject: &ScanSchedulers, - scanner: PayableSequenceScanner, - ) { - let result = subject - .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error( - scanner, - &StartScanError::NoConsumingWalletFound, - false, - ); + fn resolve_error_for_pending_p_if_no_consuming_wallet_found_in_initial_pending_payable_scan() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let scanner = PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: true, + }; - assert_eq!( - result, - SchedulingAfterHaltedScan::Schedule(ScanType::Payables), - "We expected Schedule(Payables) but got {:?} for {:?}", - result, - scanner + let result = subject + .reschedule_on_error_resolver + .resolve_rescheduling_for_given_error( + scanner, + &StartScanError::NoConsumingWalletFound, + false, ); - } + assert_eq!( + result, + StartScanErrorResponse::Schedule(ScanType::PendingPayables), + "We expected Schedule(PendingPayables) but got {:?} for {:?}", + result, + scanner + ); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: PendingPayableScanner called later \ + than the initial attempt, but the consuming wallet is still missing; this should not be \ + possible" + )] + fn pending_p_scan_attempt_if_no_consuming_wallet_found_mustnt_happen_if_not_initial_scan() { let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let scanner = PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: false, + }; - test_no_consuming_wallet_found( - &subject, - PayableSequenceScanner::PendingPayables { - initial_pending_payable_scan: false, - }, - ); - test_no_consuming_wallet_found( - &subject, - PayableSequenceScanner::PendingPayables { - initial_pending_payable_scan: true, - }, - ); + let _ = subject + .reschedule_on_error_resolver + .resolve_rescheduling_for_given_error( + scanner, + &StartScanError::NoConsumingWalletFound, + false, + ); } #[test] @@ -794,7 +815,7 @@ mod tests { assert_eq!( result, - SchedulingAfterHaltedScan::Schedule(ScanType::Payables), + StartScanErrorResponse::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got '{:?}'", result, ) From 7deca27a2570a1641b9a9c9bf45f008bc0e29677 Mon Sep 17 00:00:00 2001 From: Bert Date: Thu, 5 Jun 2025 23:31:02 +0200 Subject: [PATCH 40/49] GH-602: small functionalities made right and huge tests refactored --- node/src/accountant/mod.rs | 208 ++++++++++-------- node/src/accountant/scanners/mod.rs | 5 +- .../accountant/scanners/scan_schedulers.rs | 137 +++++++----- 3 files changed, 194 insertions(+), 156 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index f93a7ee96..3e36a704d 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -76,7 +76,7 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; -use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, SchedulingAfterHaltedScan, ScanSchedulers}; +use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, StartScanErrorResponse, ScanSchedulers}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; @@ -225,7 +225,7 @@ impl Handler for Accountant { // of the RetryPayableScanner, which finishes, and the PendingPayablesScanner is scheduled // to run again. However, not from here. let response_skeleton_opt = msg.response_skeleton_opt; - if let SchedulingAfterHaltedScan::Schedule(ScanType::Payables) = + if let StartScanErrorResponse::Schedule(ScanType::Payables) = self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) { self.scan_schedulers @@ -245,7 +245,7 @@ impl Handler for Accountant { // the PendingPayableScanner whose job is to evaluate if it has seen every pending payable // complete. That's the moment when another run of the NewPayableScanner makes sense again. let response_skeleton = msg.response_skeleton_opt; - if let SchedulingAfterHaltedScan::Schedule(ScanType::Payables) = + if let StartScanErrorResponse::Schedule(ScanType::Payables) = self.handle_request_of_scan_for_new_payable(response_skeleton) { self.scan_schedulers @@ -893,7 +893,7 @@ impl Accountant { fn handle_request_of_scan_for_new_payable( &mut self, response_skeleton_opt: Option, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( @@ -913,7 +913,7 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - SchedulingAfterHaltedScan::DoNotSchedule + StartScanErrorResponse::DoNotSchedule } Err(e) => self.handle_start_scan_error_and_prevent_scan_stall_point( PayableSequenceScanner::NewPayables, @@ -926,7 +926,7 @@ impl Accountant { fn handle_request_of_scan_for_retry_payable( &mut self, response_skeleton_opt: Option, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_retry_payable_scan_guarded( @@ -945,7 +945,7 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - SchedulingAfterHaltedScan::DoNotSchedule + StartScanErrorResponse::DoNotSchedule } Err(e) => self.handle_start_scan_error_and_prevent_scan_stall_point( PayableSequenceScanner::RetryPayables, @@ -958,7 +958,7 @@ impl Accountant { fn handle_request_of_scan_for_pending_payable( &mut self, response_skeleton_opt: Option, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_pending_payable_scan_guarded( @@ -971,14 +971,14 @@ impl Accountant { None => Err(StartScanError::NoConsumingWalletFound), }; - let hint: SchedulingAfterHaltedScan = match result { + let hint: StartScanErrorResponse = match result { Ok(scan_message) => { self.request_transaction_receipts_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - SchedulingAfterHaltedScan::DoNotSchedule + StartScanErrorResponse::DoNotSchedule } Err(e) => { let initial_pending_payable_scan = self.scanners.initial_pending_payable_scan(); @@ -1004,7 +1004,7 @@ impl Accountant { scanner: PayableSequenceScanner, e: StartScanError, response_skeleton_opt: Option, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { let is_externally_triggered = response_skeleton_opt.is_some(); e.log_error(&self.logger, scanner.into(), is_externally_triggered); @@ -3231,7 +3231,7 @@ mod tests { System::current().stop(); system.run(); let flag_after = subject.scanners.initial_pending_payable_scan(); - assert_eq!(hint, SchedulingAfterHaltedScan::DoNotSchedule); + assert_eq!(hint, StartScanErrorResponse::DoNotSchedule); assert_eq!(flag_before, true); assert_eq!(flag_after, false); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); @@ -3251,10 +3251,7 @@ mod tests { let hint = subject.handle_request_of_scan_for_pending_payable(None); let flag_after = subject.scanners.initial_pending_payable_scan(); - assert_eq!( - hint, - SchedulingAfterHaltedScan::Schedule(ScanType::Payables) - ); + assert_eq!(hint, StartScanErrorResponse::Schedule(ScanType::Payables)); assert_eq!(flag_before, true); assert_eq!(flag_after, false); } @@ -3262,7 +3259,7 @@ mod tests { #[test] #[should_panic( expected = "internal error: entered unreachable code: ScanAlreadyRunning { \ - pertinent_scanner: PendingPayables, started_at: SystemTime { tv_sec: 0, tv_nsec: 0 } } \ + cross_scan_cause_opt: None, started_at: SystemTime { tv_sec: 0, tv_nsec: 0 } } \ should be impossible with PendingPayableScanner in automatic mode" )] fn initial_pending_payable_scan_hits_unexpected_error() { @@ -3400,20 +3397,20 @@ mod tests { #[test] fn periodical_scanning_for_payables_works() { init_test_logging(); - let test_name = "periodical_scanning_for_payable_works"; + let test_name = "periodical_scanning_for_payables_works"; let start_scan_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); let start_scan_payable_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); let notify_payable_params_arc = Arc::new(Mutex::new(vec![])); let system = System::new(test_name); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge_addr = blockchain_bridge.start(); let payable_account = make_payable_account(123); let qualified_payable = vec![payable_account.clone()]; let consuming_wallet = make_paying_wallet(b"consuming"); - let agent = BlockchainAgentMock::default(); let counter_msg_1 = BlockchainAgentWithContextMessage { qualified_payables: qualified_payable.clone(), - agent: Box::new(agent), + agent: Box::new(BlockchainAgentMock::default()), response_skeleton_opt: None, }; let transaction_hash = make_tx_hash(789); @@ -3439,73 +3436,26 @@ mod tests { )], response_skeleton_opt: None, }; - let blockchain_bridge_addr = blockchain_bridge.start(); - let request_transaction_receipts = RequestTransactionReceipts { + let request_transaction_receipts_msg = RequestTransactionReceipts { pending_payable_fingerprints: vec![pending_payable_fingerprint], response_skeleton_opt: None, }; - let pending_payable_scanner = ScannerMock::new() - .scan_started_at_result(None) - .start_scan_params(&start_scan_pending_payable_params_arc) - .start_scan_result(Ok(request_transaction_receipts.clone())) - .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); let qualified_payables_msg = QualifiedPayablesMessage { qualified_payables: qualified_payable.clone(), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: None, }; - let payable_scanner = ScannerMock::new() - .scan_started_at_result(None) - // Always checking also on the payable scanner when handling ScanForPendingPayable - .scan_started_at_result(None) - .start_scan_params(&start_scan_payable_params_arc) - .start_scan_result(Ok(qualified_payables_msg.clone())) - .finish_scan_result(PayableScanResult { - ui_response_opt: None, - result: OperationOutcome::NewPendingPayable, - }); - let mut config = bc_from_earning_wallet(make_wallet("hi")); - config.scan_intervals_opt = Some(ScanIntervals { - // This simply means that we're gonna surplus this value (it abides by how many pending - // payable cycles have to go in between before the lastly submitted txs are confirmed), - payable_scan_interval: Duration::from_millis(10), - pending_payable_scan_interval: Duration::from_millis(50), - receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner - }); - let mut subject = AccountantBuilder::default() - .bootstrapper_config(config) - .consuming_wallet(consuming_wallet.clone()) - .logger(Logger::new(test_name)) - .build(); - subject - .scanners - .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( - pending_payable_scanner, - ))); - subject - .scanners - .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( - payable_scanner, - ))); - subject - .scanners - .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); //skipping - subject.scan_schedulers.pending_payable.handle = Box::new( - NotifyLaterHandleMock::::default() - .notify_later_params(¬ify_later_pending_payables_params_arc) - .capture_msg_and_let_it_fly_on(), - ); - subject.scan_schedulers.payable.new_payable_notify = Box::new( - NotifyHandleMock::::default() - .notify_params(¬ify_payable_params_arc) - // This should stop the system. If anything goes wrong, the SystemKillerActor will. - .stop_system_on_count_received(1), + let subject = set_up_subject_to_prove_periodical_payable_scan( + test_name, + &blockchain_bridge_addr, + &consuming_wallet, + &qualified_payables_msg, + &request_transaction_receipts_msg, + &start_scan_pending_payable_params_arc, + &start_scan_payable_params_arc, + ¬ify_later_pending_payables_params_arc, + ¬ify_payable_params_arc, ); - subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.clone().recipient()); - subject.outbound_payments_instructions_sub_opt = - Some(blockchain_bridge_addr.clone().recipient()); - subject.request_transaction_receipts_sub_opt = - Some(blockchain_bridge_addr.clone().recipient()); let subject_addr = subject.start(); let set_up_counter_msgs = SetUpCounterMsgs::new(vec![ setup_for_counter_msg_triggered_via_type_id!( @@ -3537,7 +3487,6 @@ mod tests { let time_before = SystemTime::now(); system.run(); let time_after = SystemTime::now(); - let tlh = TestLogHandler::new(); let mut start_scan_payable_params = start_scan_payable_params_arc.lock().unwrap(); let (wallet, timestamp, response_skeleton_opt, logger, _) = start_scan_payable_params.remove(0); @@ -3545,7 +3494,7 @@ mod tests { assert!(time_before <= timestamp && timestamp <= time_after); assert_eq!(response_skeleton_opt, None); assert!(start_scan_payable_params.is_empty()); - assert_using_the_same_logger(&logger, test_name, Some("abc")); + assert_using_the_same_logger(&logger, test_name, Some("start scan payable")); let mut start_scan_pending_payable_params = start_scan_pending_payable_params_arc.lock().unwrap(); let (wallet, timestamp, response_skeleton_opt, logger, _) = @@ -3554,7 +3503,7 @@ mod tests { assert!(time_before <= timestamp && timestamp <= time_after); assert_eq!(response_skeleton_opt, None); assert!(start_scan_pending_payable_params.is_empty()); - assert_using_the_same_logger(&logger, test_name, Some("def")); + assert_using_the_same_logger(&logger, test_name, Some("start scan pending payable")); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); let actual_qualified_payables_msg = blockchain_bridge_recording.get_record::(0); @@ -3567,7 +3516,10 @@ mod tests { ); let actual_requested_receipts_1 = blockchain_bridge_recording.get_record::(2); - assert_eq!(actual_requested_receipts_1, &request_transaction_receipts); + assert_eq!( + actual_requested_receipts_1, + &request_transaction_receipts_msg + ); let notify_later_pending_payables_params = notify_later_pending_payables_params_arc.lock().unwrap(); assert_eq!( @@ -3586,12 +3538,83 @@ mod tests { response_skeleton_opt: None },] ); - tlh.exists_log_containing(&format!( - "DEBUG: {test_name}: verifying payable scanner logger" - )); - tlh.exists_log_containing(&format!( - "DEBUG: {test_name}: verifying pending payable scanner logger" - )); + } + + fn set_up_subject_to_prove_periodical_payable_scan( + test_name: &str, + blockchain_bridge_addr: &Addr, + consuming_wallet: &Wallet, + qualified_payables_msg: &QualifiedPayablesMessage, + request_transaction_receipts: &RequestTransactionReceipts, + start_scan_pending_payable_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + start_scan_payable_params_arc: &Arc< + Mutex, Logger, String)>>, + >, + notify_later_pending_payables_params_arc: &Arc< + Mutex>, + >, + notify_payable_params_arc: &Arc>>, + ) -> Accountant { + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&start_scan_pending_payable_params_arc) + .start_scan_result(Ok(request_transaction_receipts.clone())) + .finish_scan_result(PendingPayableScanResult::NoPendingPayablesLeft(None)); + let payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + // Always checking also on the payable scanner when handling ScanForPendingPayable + .scan_started_at_result(None) + .start_scan_params(&start_scan_payable_params_arc) + .start_scan_result(Ok(qualified_payables_msg.clone())) + .finish_scan_result(PayableScanResult { + ui_response_opt: None, + result: OperationOutcome::NewPendingPayable, + }); + let mut config = bc_from_earning_wallet(make_wallet("hi")); + config.scan_intervals_opt = Some(ScanIntervals { + // This simply means that we're gonna surplus this value (it abides by how many pending + // payable cycles have to go in between before the lastly submitted txs are confirmed), + payable_scan_interval: Duration::from_millis(10), + pending_payable_scan_interval: Duration::from_millis(50), + receivable_scan_interval: Duration::from_secs(100), // We'll never run this scanner + }); + let mut subject = AccountantBuilder::default() + .bootstrapper_config(config) + .consuming_wallet(consuming_wallet.clone()) + .logger(Logger::new(test_name)) + .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Null)); //skipping + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::::default() + .notify_later_params(¬ify_later_pending_payables_params_arc) + .capture_msg_and_let_it_fly_on(), + ); + subject.scan_schedulers.payable.new_payable_notify = Box::new( + NotifyHandleMock::::default() + .notify_params(¬ify_payable_params_arc) + // This should stop the system. If anything goes wrong, the SystemKillerActor will. + .stop_system_on_count_received(1), + ); + subject.qualified_payables_sub_opt = Some(blockchain_bridge_addr.clone().recipient()); + subject.outbound_payments_instructions_sub_opt = + Some(blockchain_bridge_addr.clone().recipient()); + subject.request_transaction_receipts_sub_opt = + Some(blockchain_bridge_addr.clone().recipient()); + subject } #[test] @@ -3610,7 +3633,7 @@ mod tests { .is_some(); assert_eq!(has_scan_started, false); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: Cannot initiate Payables scan because no consuming wallet was found." + "WARN: {test_name}: Cannot initiate Payables scan because no consuming wallet was found." )); } @@ -3630,7 +3653,7 @@ mod tests { .is_some(); assert_eq!(has_scan_started, false); TestLogHandler::new().exists_log_containing(&format!( - "DEBUG: {test_name}: Cannot initiate PendingPayables scan because no consuming wallet was found." + "WARN: {test_name}: Cannot initiate PendingPayables scan because no consuming wallet was found." )); } @@ -3749,10 +3772,7 @@ mod tests { System::current().stop(); system.run(); - assert_eq!( - result, - SchedulingAfterHaltedScan::Schedule(ScanType::Payables) - ); + assert_eq!(result, StartScanErrorResponse::Schedule(ScanType::Payables)); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recordings.len(), 0); } diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 88d186849..58c49002b 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -904,10 +904,7 @@ impl Scanner for PendingPay let requires_payment_retry = match message.fingerprints_with_receipts.is_empty() { true => { warning!(logger, "No transaction receipts found."); - todo!( - "This requires the payment retry. It must be processed by using some of the new \ - methods on the SentPaybleDAO. After GH-631 is done." - ); + todo!("This requires the payment retry. GH-631 must be completed first"); } false => { debug!( diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index e0724cf5f..abdc9bffa 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -44,7 +44,7 @@ pub enum PayableScanSchedulerError { } #[derive(Debug, PartialEq)] -pub enum SchedulingAfterHaltedScan { +pub enum StartScanErrorResponse { Schedule(ScanType), DoNotSchedule, } @@ -122,9 +122,9 @@ impl PayableScanScheduler { } } - // This message is always inserted into the Accountant's mailbox with no delay. - // Can also be triggered by command, following up after the PendingPayableScanner that - // requests it. That's why the response skeleton is possible to be used. + // This message ships into the Accountant's mailbox with no delay. + // Can also be triggered by command, following up after the PendingPayableScanner + // that requests it. That's why the response skeleton is possible to be used. pub fn schedule_retry_payable_scan( &self, ctx: &mut Context, @@ -221,19 +221,20 @@ where } } -// Scanners that conclude by scheduling a later scan (usually different from this one) must handle -// StartScanErrors carefully to maintain continuity and periodicity. Poor handling could disrupt -// the entire scan chain. Where possible, a different type of scan may be scheduled (avoiding -// repetition of the erroneous scan) to prevent a full panic, while ensuring no unresolved issues -// are left for future scans. A panic is justified only if the error is deemed impossible by design -// within the broader context of that location. +// Scanners that take part in a scan sequence composed of different scanners must handle +// StartScanErrors delicately to maintain the continuity and periodicity of this process. Where +// possible, either the same, some other, but traditional, or even a totally unrelated scan chosen +// just in the event of emergency, may be scheduled. The intention is to prevent a full panic while +// ensuring no harmful, toxic issues are left behind for the future scans. Following that philosophy, +// panic is justified only if the error was thought to be impossible by design and contextual +// things but still happened. pub trait RescheduleScanOnErrorResolver { fn resolve_rescheduling_for_given_error( &self, scanner: PayableSequenceScanner, error: &StartScanError, is_externally_triggered: bool, - ) -> SchedulingAfterHaltedScan; + ) -> StartScanErrorResponse; } #[derive(Default)] @@ -245,7 +246,7 @@ impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverReal { scanner: PayableSequenceScanner, error: &StartScanError, is_externally_triggered: bool, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { match scanner { PayableSequenceScanner::NewPayables => { Self::resolve_new_payables(error, is_externally_triggered) @@ -268,28 +269,28 @@ impl RescheduleScanOnErrorResolverReal { fn resolve_new_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { if is_externally_triggered { - SchedulingAfterHaltedScan::DoNotSchedule + StartScanErrorResponse::DoNotSchedule } else if matches!(err, StartScanError::ScanAlreadyRunning { .. }) { unreachable!( "an automatic scan of NewPayableScanner should never interfere with itself {:?}", err ) } else { - SchedulingAfterHaltedScan::Schedule(ScanType::Payables) + StartScanErrorResponse::Schedule(ScanType::Payables) } } - // This looks paradoxical, but this scanner is meant to be shielded by the scanner right before + // Paradoxical at first, but this scanner is meant to be shielded by the scanner right before // it. That should ensure this scanner will not be requested if there was already something - // fishy. We can go strict. + // fishy. We can impose strictness. fn resolve_retry_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { if is_externally_triggered { - SchedulingAfterHaltedScan::DoNotSchedule + StartScanErrorResponse::DoNotSchedule } else { unreachable!( "{:?} should be impossible with RetryPayableScanner in automatic mode", @@ -302,12 +303,12 @@ impl RescheduleScanOnErrorResolverReal { err: &StartScanError, initial_pending_payable_scan: bool, is_externally_triggered: bool, - ) -> SchedulingAfterHaltedScan { + ) -> StartScanErrorResponse { if is_externally_triggered { - SchedulingAfterHaltedScan::DoNotSchedule + StartScanErrorResponse::DoNotSchedule } else if err == &StartScanError::NothingToProcess { if initial_pending_payable_scan { - SchedulingAfterHaltedScan::Schedule(ScanType::Payables) + StartScanErrorResponse::Schedule(ScanType::Payables) } else { unreachable!( "the automatic pending payable scan should always be requested only in need, \ @@ -315,7 +316,21 @@ impl RescheduleScanOnErrorResolverReal { ) } } else if err == &StartScanError::NoConsumingWalletFound { - SchedulingAfterHaltedScan::Schedule(ScanType::Payables) + if initial_pending_payable_scan { + // Cannot deduce there are strayed pending payables from the previous Node's run + // (StartScanError::NoConsumingWalletFound is thrown before + // StartScanError::NothingToProcess can be evaluated); but may be cautious and + // prevent starting the NewPayableScanner. Repeating this scan endlessly may alarm + // the user. + // TODO Correctly, a check-point during the bootstrap should be the solution, + // which wouldn't allow to come this far. + StartScanErrorResponse::Schedule(ScanType::PendingPayables) + } else { + unreachable!( + "PendingPayableScanner called later than the initial attempt, but \ + the consuming wallet is still missing; this should not be possible" + ) + } } else { unreachable!( "{:?} should be impossible with PendingPayableScanner in automatic mode", @@ -329,7 +344,7 @@ impl RescheduleScanOnErrorResolverReal { mod tests { use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, - PayableSequenceScanner, ScanSchedulers, SchedulingAfterHaltedScan, + PayableSequenceScanner, ScanSchedulers, StartScanErrorResponse, }; use crate::accountant::scanners::{MTError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; @@ -572,7 +587,7 @@ mod tests { assert_eq!( result, - SchedulingAfterHaltedScan::DoNotSchedule, + StartScanErrorResponse::DoNotSchedule, "We expected DoNotSchedule but got {:?} at idx {} for {:?}", result, idx, @@ -598,7 +613,7 @@ mod tests { assert_eq!( result, - SchedulingAfterHaltedScan::Schedule(ScanType::Payables), + StartScanErrorResponse::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got {:?}", result, ); @@ -626,42 +641,48 @@ mod tests { } #[test] - fn resolve_error_for_pending_payables_if_no_consuming_wallet_found() { - fn test_no_consuming_wallet_found( - subject: &ScanSchedulers, - scanner: PayableSequenceScanner, - ) { - let result = subject - .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error( - scanner, - &StartScanError::NoConsumingWalletFound, - false, - ); + fn resolve_error_for_pending_p_if_no_consuming_wallet_found_in_initial_pending_payable_scan() { + let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let scanner = PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: true, + }; - assert_eq!( - result, - SchedulingAfterHaltedScan::Schedule(ScanType::Payables), - "We expected Schedule(Payables) but got {:?} for {:?}", - result, - scanner + let result = subject + .reschedule_on_error_resolver + .resolve_rescheduling_for_given_error( + scanner, + &StartScanError::NoConsumingWalletFound, + false, ); - } + assert_eq!( + result, + StartScanErrorResponse::Schedule(ScanType::PendingPayables), + "We expected Schedule(PendingPayables) but got {:?} for {:?}", + result, + scanner + ); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: PendingPayableScanner called later \ + than the initial attempt, but the consuming wallet is still missing; this should not be \ + possible" + )] + fn pending_p_scan_attempt_if_no_consuming_wallet_found_mustnt_happen_if_not_initial_scan() { let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let scanner = PayableSequenceScanner::PendingPayables { + initial_pending_payable_scan: false, + }; - test_no_consuming_wallet_found( - &subject, - PayableSequenceScanner::PendingPayables { - initial_pending_payable_scan: false, - }, - ); - test_no_consuming_wallet_found( - &subject, - PayableSequenceScanner::PendingPayables { - initial_pending_payable_scan: true, - }, - ); + let _ = subject + .reschedule_on_error_resolver + .resolve_rescheduling_for_given_error( + scanner, + &StartScanError::NoConsumingWalletFound, + false, + ); } #[test] @@ -794,7 +815,7 @@ mod tests { assert_eq!( result, - SchedulingAfterHaltedScan::Schedule(ScanType::Payables), + StartScanErrorResponse::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got '{:?}'", result, ) From c548d18b71550602a400d6ea7c9752a0a4f46079 Mon Sep 17 00:00:00 2001 From: Bert Date: Fri, 6 Jun 2025 00:14:59 +0200 Subject: [PATCH 41/49] GH-602: prepared some fixes for early rescheduling --- node/src/accountant/mod.rs | 57 ++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 3e36a704d..f3662f62c 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -225,12 +225,23 @@ impl Handler for Accountant { // of the RetryPayableScanner, which finishes, and the PendingPayablesScanner is scheduled // to run again. However, not from here. let response_skeleton_opt = msg.response_skeleton_opt; - if let StartScanErrorResponse::Schedule(ScanType::Payables) = - self.handle_request_of_scan_for_pending_payable(response_skeleton_opt) - { - self.scan_schedulers + + let scheduling_hint = + self.handle_request_of_scan_for_pending_payable(response_skeleton_opt); + + match scheduling_hint { + StartScanErrorResponse::Schedule(ScanType::Payables) => self + .scan_schedulers .payable - .schedule_new_payable_scan(ctx, &self.logger); + .schedule_new_payable_scan(ctx, &self.logger), + StartScanErrorResponse::Schedule(ScanType::PendingPayables) => todo!(), + StartScanErrorResponse::Schedule(_) => todo!(), + StartScanErrorResponse::DoNotSchedule => { + trace!( + self.logger, + "No early rescheduling, as the pending payable scan found results" + ); + } } } } @@ -245,12 +256,21 @@ impl Handler for Accountant { // the PendingPayableScanner whose job is to evaluate if it has seen every pending payable // complete. That's the moment when another run of the NewPayableScanner makes sense again. let response_skeleton = msg.response_skeleton_opt; - if let StartScanErrorResponse::Schedule(ScanType::Payables) = - self.handle_request_of_scan_for_new_payable(response_skeleton) - { - self.scan_schedulers + + let scheduling_hint = self.handle_request_of_scan_for_new_payable(response_skeleton); + + match scheduling_hint { + StartScanErrorResponse::Schedule(ScanType::Payables) => self + .scan_schedulers .payable - .schedule_new_payable_scan(ctx, &self.logger) + .schedule_new_payable_scan(ctx, &self.logger), + StartScanErrorResponse::Schedule(other_scan_type) => todo!(), + StartScanErrorResponse::DoNotSchedule => { + trace!( + self.logger, + "No early rescheduling, as the new payable scan found results" + ) + } } } } @@ -262,7 +282,7 @@ impl Handler for Accountant { // RetryPayableScanner is scheduled only when the PendingPayableScanner finishes discovering // that there have been some failed payables. No place for that here. let response_skeleton = msg.response_skeleton_opt; - let _ = self.handle_request_of_scan_for_retry_payable(response_skeleton); + self.handle_request_of_scan_for_retry_payable(response_skeleton); } } @@ -926,7 +946,7 @@ impl Accountant { fn handle_request_of_scan_for_retry_payable( &mut self, response_skeleton_opt: Option, - ) -> StartScanErrorResponse { + ) { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_retry_payable_scan_guarded( @@ -945,13 +965,14 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - StartScanErrorResponse::DoNotSchedule } - Err(e) => self.handle_start_scan_error_and_prevent_scan_stall_point( - PayableSequenceScanner::RetryPayables, - e, - response_skeleton_opt, - ), + Err(e) => { + let _ = self.handle_start_scan_error_and_prevent_scan_stall_point( + PayableSequenceScanner::RetryPayables, + e, + response_skeleton_opt, + ); + } } } From 6f904e62e003a383dd1fc5ab8c2c4e6d10513748 Mon Sep 17 00:00:00 2001 From: Bert Date: Fri, 6 Jun 2025 00:20:51 +0200 Subject: [PATCH 42/49] GH-602: moved the scanner test utils --- node/src/accountant/mod.rs | 3 +- node/src/accountant/scanners/mod.rs | 313 +-------------------- node/src/accountant/scanners/test_utils.rs | 289 ++++++++++++++++++- 3 files changed, 294 insertions(+), 311 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index f3662f62c..24b60bb8f 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1215,7 +1215,7 @@ mod tests { use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp, CustomQuery}; use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::{MarkScanner, NewPayableScanDynIntervalComputerMock, ReplacementType, ScannerReplacement}; + use crate::accountant::scanners::test_utils::{MarkScanner, NewPayableScanDynIntervalComputerMock, ReplacementType, ScannerMock, ScannerReplacement}; use crate::accountant::scanners::{StartScanError}; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, @@ -1275,7 +1275,6 @@ mod tests { use std::sync::Mutex; use std::time::{Duration, UNIX_EPOCH}; use std::vec; - use crate::accountant::scanners::local_test_utils::{ScannerMock}; use crate::accountant::scanners::scan_schedulers::{NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 58c49002b..27d6082fb 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -360,9 +360,13 @@ impl Scanners { } } -trait PrivateScanner: - StartableScanner + Scanner -where +pub(in crate::accountant::scanners) trait PrivateScanner< + TriggerMessage, + StartMessage, + EndMessage, + ScanResult, +>: + StartableScanner + Scanner where TriggerMessage: Message, StartMessage: Message, EndMessage: Message, @@ -1384,306 +1388,6 @@ macro_rules! impl_real_scanner_marker { impl_real_scanner_marker!(PayableScanner, PendingPayableScanner, ReceivableScanner); -// Note that this location was chosen because the following mocks need to implement a private trait -// from this file -#[cfg(test)] -pub mod local_test_utils { - use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; - use crate::accountant::scanners::{ - MultistageDualPayableScanner, PreparedAdjustment, PrivateScanner, Scanner, - SolvencySensitivePaymentInstructor, StartScanError, StartableScanner, - }; - use crate::accountant::BlockchainAgentWithContextMessage; - use crate::accountant::OutboundPaymentsInstructions; - use crate::accountant::{ResponseSkeleton, SentPayables}; - use crate::sub_lib::wallet::Wallet; - use actix::{Message, System}; - use itertools::Either; - use masq_lib::logger::Logger; - use std::any::type_name; - use std::cell::RefCell; - use std::sync::{Arc, Mutex}; - use std::time::SystemTime; - - pub struct NullScanner {} - - impl - PrivateScanner for NullScanner - where - TriggerMessage: Message, - StartMessage: Message, - EndMessage: Message, - { - } - - impl StartableScanner for NullScanner - where - TriggerMessage: Message, - StartMessage: Message, - { - fn start_scan( - &mut self, - _wallet: &Wallet, - _timestamp: SystemTime, - _response_skeleton_opt: Option, - _logger: &Logger, - ) -> Result { - Err(StartScanError::CalledFromNullScanner) - } - } - - impl Scanner for NullScanner - where - EndMessage: Message, - { - fn finish_scan(&mut self, _message: EndMessage, _logger: &Logger) -> ScanResult { - panic!("Called finish_scan() from NullScanner"); - } - - fn scan_started_at(&self) -> Option { - None - } - - fn mark_as_started(&mut self, _timestamp: SystemTime) { - panic!("Called mark_as_started() from NullScanner"); - } - - fn mark_as_ended(&mut self, _logger: &Logger) { - panic!("Called mark_as_ended() from NullScanner"); - } - - as_any_ref_in_trait_impl!(); - } - - impl MultistageDualPayableScanner for NullScanner {} - - impl SolvencySensitivePaymentInstructor for NullScanner { - fn try_skipping_payment_adjustment( - &self, - _msg: BlockchainAgentWithContextMessage, - _logger: &Logger, - ) -> Result, String> { - intentionally_blank!() - } - - fn perform_payment_adjustment( - &self, - _setup: PreparedAdjustment, - _logger: &Logger, - ) -> OutboundPaymentsInstructions { - intentionally_blank!() - } - } - - impl Default for NullScanner { - fn default() -> Self { - Self::new() - } - } - - impl NullScanner { - pub fn new() -> Self { - Self {} - } - } - - pub struct ScannerMock { - start_scan_params: - Arc, Logger, String)>>>, - start_scan_results: RefCell>>, - finish_scan_params: Arc>>, - finish_scan_results: RefCell>, - scan_started_at_results: RefCell>>, - stop_system_after_last_message: RefCell, - } - - impl - PrivateScanner - for ScannerMock - where - TriggerMessage: Message, - StartMessage: Message, - EndMessage: Message, - { - } - - impl - StartableScanner - for ScannerMock - where - TriggerMessage: Message, - StartMessage: Message, - EndMessage: Message, - { - fn start_scan( - &mut self, - wallet: &Wallet, - timestamp: SystemTime, - response_skeleton_opt: Option, - logger: &Logger, - ) -> Result { - self.start_scan_params.lock().unwrap().push(( - wallet.clone(), - timestamp, - response_skeleton_opt, - logger.clone(), - // This serves for identification in scanners allowing different modes to start - // them up through. - type_name::().to_string(), - )); - if self.is_allowed_to_stop_the_system() && self.is_last_message() { - System::current().stop(); - } - self.start_scan_results.borrow_mut().remove(0) - } - } - - impl Scanner - for ScannerMock - where - StartMessage: Message, - EndMessage: Message, - { - fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> ScanResult { - self.finish_scan_params - .lock() - .unwrap() - .push((message, logger.clone())); - if self.is_allowed_to_stop_the_system() && self.is_last_message() { - System::current().stop(); - } - self.finish_scan_results.borrow_mut().remove(0) - } - - fn scan_started_at(&self) -> Option { - self.scan_started_at_results.borrow_mut().remove(0) - } - - fn mark_as_started(&mut self, _timestamp: SystemTime) { - intentionally_blank!() - } - - fn mark_as_ended(&mut self, _logger: &Logger) { - intentionally_blank!() - } - } - - impl Default - for ScannerMock - { - fn default() -> Self { - Self::new() - } - } - - impl ScannerMock { - pub fn new() -> Self { - Self { - start_scan_params: Arc::new(Mutex::new(vec![])), - start_scan_results: RefCell::new(vec![]), - finish_scan_params: Arc::new(Mutex::new(vec![])), - finish_scan_results: RefCell::new(vec![]), - scan_started_at_results: RefCell::new(vec![]), - stop_system_after_last_message: RefCell::new(false), - } - } - - pub fn start_scan_params( - mut self, - params: &Arc< - Mutex, Logger, String)>>, - >, - ) -> Self { - self.start_scan_params = params.clone(); - self - } - - pub fn start_scan_result(self, result: Result) -> Self { - self.start_scan_results.borrow_mut().push(result); - self - } - - pub fn scan_started_at_result(self, result: Option) -> Self { - self.scan_started_at_results.borrow_mut().push(result); - self - } - - pub fn finish_scan_params( - mut self, - params: &Arc>>, - ) -> Self { - self.finish_scan_params = params.clone(); - self - } - - pub fn finish_scan_result(self, result: ScanResult) -> Self { - self.finish_scan_results.borrow_mut().push(result); - self - } - - pub fn stop_the_system_after_last_msg(self) -> Self { - self.stop_system_after_last_message.replace(true); - self - } - - pub fn is_allowed_to_stop_the_system(&self) -> bool { - *self.stop_system_after_last_message.borrow() - } - - pub fn is_last_message(&self) -> bool { - self.is_last_message_from_start_scan() || self.is_last_message_from_end_scan() - } - - pub fn is_last_message_from_start_scan(&self) -> bool { - self.start_scan_results.borrow().len() == 1 - && self.finish_scan_results.borrow().is_empty() - } - - pub fn is_last_message_from_end_scan(&self) -> bool { - self.finish_scan_results.borrow().len() == 1 - && self.start_scan_results.borrow().is_empty() - } - } - - impl MultistageDualPayableScanner - for ScannerMock - { - } - - impl SolvencySensitivePaymentInstructor - for ScannerMock - { - fn try_skipping_payment_adjustment( - &self, - msg: BlockchainAgentWithContextMessage, - _logger: &Logger, - ) -> Result, String> { - // Always passes... - // It would be quite inconvenient if we had to add specialized features to the generic - // mock, plus this functionality can be tested better with the other components mocked, - // not the scanner itself. - Ok(Either::Left(OutboundPaymentsInstructions { - affordable_accounts: msg.qualified_payables, - agent: msg.agent, - response_skeleton_opt: msg.response_skeleton_opt, - })) - } - - fn perform_payment_adjustment( - &self, - _setup: PreparedAdjustment, - _logger: &Logger, - ) -> OutboundPaymentsInstructions { - intentionally_blank!() - } - } - - pub trait ScannerMockMarker {} - - impl ScannerMockMarker for ScannerMock {} -} - #[cfg(test)] mod tests { use crate::accountant::db_access_objects::payable_dao::{PayableAccount, PayableDaoError}; @@ -1731,8 +1435,7 @@ mod tests { use web3::Error; use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeToUiMessage; - use crate::accountant::scanners::local_test_utils::{NullScanner}; - use crate::accountant::scanners::test_utils::{assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, ReplacementType, ScannerReplacement}; + use crate::accountant::scanners::test_utils::{assert_timestamps_from_str, parse_system_time_from_str, MarkScanner, NullScanner, ReplacementType, ScannerReplacement}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TransactionReceiptResult, TxReceipt, TxStatus}; impl Scanners { diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index e0a52683f..963702ed3 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -2,26 +2,307 @@ #![cfg(test)] -use crate::accountant::scanners::local_test_utils::{ScannerMock, ScannerMockMarker}; -use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + BlockchainAgentWithContextMessage, QualifiedPayablesMessage, +}; +use crate::accountant::scanners::payable_scanner_extension::{ + MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, +}; use crate::accountant::scanners::scan_schedulers::NewPayableScanDynIntervalComputer; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::accountant::scanners::{ - PayableScanner, PendingPayableScanner, RealScannerMarker, ReceivableScanner, + PayableScanner, PendingPayableScanner, PrivateScanner, RealScannerMarker, ReceivableScanner, + Scanner, StartScanError, StartableScanner, }; use crate::accountant::{ - ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, SentPayables, + ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, + SentPayables, }; use crate::blockchain::blockchain_bridge::RetrieveTransactions; +use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; +use crate::sub_lib::wallet::Wallet; +use actix::{Message, System}; +use itertools::Either; use masq_lib::logger::{Logger, TIME_FORMATTING_STRING}; use masq_lib::ui_gateway::NodeToUiMessage; use regex::Regex; +use std::any::type_name; use std::cell::RefCell; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use time::{format_description, PrimitiveDateTime}; +pub struct NullScanner {} + +impl + PrivateScanner for NullScanner +where + TriggerMessage: Message, + StartMessage: Message, + EndMessage: Message, +{ +} + +impl StartableScanner for NullScanner +where + TriggerMessage: Message, + StartMessage: Message, +{ + fn start_scan( + &mut self, + _wallet: &Wallet, + _timestamp: SystemTime, + _response_skeleton_opt: Option, + _logger: &Logger, + ) -> Result { + Err(StartScanError::CalledFromNullScanner) + } +} + +impl Scanner for NullScanner +where + EndMessage: Message, +{ + fn finish_scan(&mut self, _message: EndMessage, _logger: &Logger) -> ScanResult { + panic!("Called finish_scan() from NullScanner"); + } + + fn scan_started_at(&self) -> Option { + None + } + + fn mark_as_started(&mut self, _timestamp: SystemTime) { + panic!("Called mark_as_started() from NullScanner"); + } + + fn mark_as_ended(&mut self, _logger: &Logger) { + panic!("Called mark_as_ended() from NullScanner"); + } + + as_any_ref_in_trait_impl!(); +} + +impl MultistageDualPayableScanner for NullScanner {} + +impl SolvencySensitivePaymentInstructor for NullScanner { + fn try_skipping_payment_adjustment( + &self, + _msg: BlockchainAgentWithContextMessage, + _logger: &Logger, + ) -> Result, String> { + intentionally_blank!() + } + + fn perform_payment_adjustment( + &self, + _setup: PreparedAdjustment, + _logger: &Logger, + ) -> OutboundPaymentsInstructions { + intentionally_blank!() + } +} + +impl Default for NullScanner { + fn default() -> Self { + Self::new() + } +} + +impl NullScanner { + pub fn new() -> Self { + Self {} + } +} + +pub struct ScannerMock { + start_scan_params: + Arc, Logger, String)>>>, + start_scan_results: RefCell>>, + finish_scan_params: Arc>>, + finish_scan_results: RefCell>, + scan_started_at_results: RefCell>>, + stop_system_after_last_message: RefCell, +} + +impl + PrivateScanner + for ScannerMock +where + TriggerMessage: Message, + StartMessage: Message, + EndMessage: Message, +{ +} + +impl + StartableScanner + for ScannerMock +where + TriggerMessage: Message, + StartMessage: Message, + EndMessage: Message, +{ + fn start_scan( + &mut self, + wallet: &Wallet, + timestamp: SystemTime, + response_skeleton_opt: Option, + logger: &Logger, + ) -> Result { + self.start_scan_params.lock().unwrap().push(( + wallet.clone(), + timestamp, + response_skeleton_opt, + logger.clone(), + // This serves for identification in scanners allowing different modes to start + // them up through. + type_name::().to_string(), + )); + if self.is_allowed_to_stop_the_system() && self.is_last_message() { + System::current().stop(); + } + self.start_scan_results.borrow_mut().remove(0) + } +} + +impl Scanner + for ScannerMock +where + StartMessage: Message, + EndMessage: Message, +{ + fn finish_scan(&mut self, message: EndMessage, logger: &Logger) -> ScanResult { + self.finish_scan_params + .lock() + .unwrap() + .push((message, logger.clone())); + if self.is_allowed_to_stop_the_system() && self.is_last_message() { + System::current().stop(); + } + self.finish_scan_results.borrow_mut().remove(0) + } + + fn scan_started_at(&self) -> Option { + self.scan_started_at_results.borrow_mut().remove(0) + } + + fn mark_as_started(&mut self, _timestamp: SystemTime) { + intentionally_blank!() + } + + fn mark_as_ended(&mut self, _logger: &Logger) { + intentionally_blank!() + } +} + +impl Default + for ScannerMock +{ + fn default() -> Self { + Self::new() + } +} + +impl ScannerMock { + pub fn new() -> Self { + Self { + start_scan_params: Arc::new(Mutex::new(vec![])), + start_scan_results: RefCell::new(vec![]), + finish_scan_params: Arc::new(Mutex::new(vec![])), + finish_scan_results: RefCell::new(vec![]), + scan_started_at_results: RefCell::new(vec![]), + stop_system_after_last_message: RefCell::new(false), + } + } + + pub fn start_scan_params( + mut self, + params: &Arc, Logger, String)>>>, + ) -> Self { + self.start_scan_params = params.clone(); + self + } + + pub fn start_scan_result(self, result: Result) -> Self { + self.start_scan_results.borrow_mut().push(result); + self + } + + pub fn scan_started_at_result(self, result: Option) -> Self { + self.scan_started_at_results.borrow_mut().push(result); + self + } + + pub fn finish_scan_params(mut self, params: &Arc>>) -> Self { + self.finish_scan_params = params.clone(); + self + } + + pub fn finish_scan_result(self, result: ScanResult) -> Self { + self.finish_scan_results.borrow_mut().push(result); + self + } + + pub fn stop_the_system_after_last_msg(self) -> Self { + self.stop_system_after_last_message.replace(true); + self + } + + pub fn is_allowed_to_stop_the_system(&self) -> bool { + *self.stop_system_after_last_message.borrow() + } + + pub fn is_last_message(&self) -> bool { + self.is_last_message_from_start_scan() || self.is_last_message_from_end_scan() + } + + pub fn is_last_message_from_start_scan(&self) -> bool { + self.start_scan_results.borrow().len() == 1 && self.finish_scan_results.borrow().is_empty() + } + + pub fn is_last_message_from_end_scan(&self) -> bool { + self.finish_scan_results.borrow().len() == 1 && self.start_scan_results.borrow().is_empty() + } +} + +impl MultistageDualPayableScanner + for ScannerMock +{ +} + +impl SolvencySensitivePaymentInstructor + for ScannerMock +{ + fn try_skipping_payment_adjustment( + &self, + msg: BlockchainAgentWithContextMessage, + _logger: &Logger, + ) -> Result, String> { + // Always passes... + // It would be quite inconvenient if we had to add specialized features to the generic + // mock, plus this functionality can be tested better with the other components mocked, + // not the scanner itself. + Ok(Either::Left(OutboundPaymentsInstructions { + affordable_accounts: msg.qualified_payables, + agent: msg.agent, + response_skeleton_opt: msg.response_skeleton_opt, + })) + } + + fn perform_payment_adjustment( + &self, + _setup: PreparedAdjustment, + _logger: &Logger, + ) -> OutboundPaymentsInstructions { + intentionally_blank!() + } +} + +pub trait ScannerMockMarker {} + +impl ScannerMockMarker for ScannerMock {} + #[derive(Default)] pub struct NewPayableScanDynIntervalComputerMock { compute_interval_params: Arc>>, From b3691579816b6d8884971e17ec778c01c49aed60 Mon Sep 17 00:00:00 2001 From: Bert Date: Fri, 6 Jun 2025 01:32:39 +0200 Subject: [PATCH 43/49] GH-602: counter message summary comment added --- node/src/test_utils/recorder_counter_msgs.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/node/src/test_utils/recorder_counter_msgs.rs b/node/src/test_utils/recorder_counter_msgs.rs index cfda62396..d0c86b9d0 100644 --- a/node/src/test_utils/recorder_counter_msgs.rs +++ b/node/src/test_utils/recorder_counter_msgs.rs @@ -9,6 +9,20 @@ use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::HashMap; +// Counter-messages are a powerful tool that allows you to actively simulate communication within +// a system. They enable sending either a single message or multiple messages in response to +// a specific trigger, which is just another Actor message arriving at the Recorder. +// By trigger, we mean the moment when an incoming message is tested sequentially against collected +// identification methods and matches. Each counter-message must have its identification method +// attached when it is being prepared for storage in the Recorder. +// Counter-messages can be independently customized and targeted at different actors by +// providing their addresses, supporting complex interaction patterns. This design facilitates +// sophisticated testing scenarios by mimicking real communication flows between multiple Actors. +// The actual preparation of the Recorder needs to be carried out somewhat specifically during the +// late stage of configuring the test, when all participating Actors are already started and their +// addresses are known. The setup for counter-messages must be registered with the appropriate +// Recorder using a specially designated Actor message called `SetUpCounterMsgs`. + pub trait CounterMsgGear: Send { fn try_send(&self); } From 62eabf465f9fdf09397a2dacaa5deb74385c9177 Mon Sep 17 00:00:00 2001 From: Bert Date: Sat, 7 Jun 2025 01:34:48 +0200 Subject: [PATCH 44/49] GH-602: probably finished the minimm of what can be submitted seriously --- node/src/accountant/mod.rs | 155 +++++++++++++--- .../accountant/scanners/scan_schedulers.rs | 175 ++++++++++++++---- node/src/accountant/scanners/test_utils.rs | 54 +++++- 3 files changed, 322 insertions(+), 62 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 24b60bb8f..ea4899697 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -76,7 +76,7 @@ use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use web3::types::H256; -use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, StartScanErrorResponse, ScanSchedulers}; +use crate::accountant::scanners::scan_schedulers::{PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::OperationOutcome; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; @@ -230,16 +230,23 @@ impl Handler for Accountant { self.handle_request_of_scan_for_pending_payable(response_skeleton_opt); match scheduling_hint { - StartScanErrorResponse::Schedule(ScanType::Payables) => self + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) => self .scan_schedulers .payable .schedule_new_payable_scan(ctx, &self.logger), - StartScanErrorResponse::Schedule(ScanType::PendingPayables) => todo!(), - StartScanErrorResponse::Schedule(_) => todo!(), - StartScanErrorResponse::DoNotSchedule => { + ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables) => self + .scan_schedulers + .pending_payable + .schedule(ctx, &self.logger), + ScanRescheduleAfterEarlyStop::Schedule(scan_type) => unreachable!( + "Early stopped pending payable scan was suggested to be followed up \ + by the scan for {:?}, which is not supported though", + scan_type + ), + ScanRescheduleAfterEarlyStop::DoNotSchedule => { trace!( self.logger, - "No early rescheduling, as the pending payable scan found results" + "No early rescheduling, as the pending payable scan did find results" ); } } @@ -260,15 +267,19 @@ impl Handler for Accountant { let scheduling_hint = self.handle_request_of_scan_for_new_payable(response_skeleton); match scheduling_hint { - StartScanErrorResponse::Schedule(ScanType::Payables) => self + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) => self .scan_schedulers .payable .schedule_new_payable_scan(ctx, &self.logger), - StartScanErrorResponse::Schedule(other_scan_type) => todo!(), - StartScanErrorResponse::DoNotSchedule => { + ScanRescheduleAfterEarlyStop::Schedule(other_scan_type) => unreachable!( + "Early stopped new payable scan was suggested to be followed up by the scan \ + for {:?}, which is not supported though", + other_scan_type + ), + ScanRescheduleAfterEarlyStop::DoNotSchedule => { trace!( self.logger, - "No early rescheduling, as the new payable scan found results" + "No early rescheduling, as the new payable scan did find results" ) } } @@ -363,8 +374,8 @@ impl Handler for Accountant { .expect("UIGateway is dead"); // Externally triggered scans are not allowed to provoke an unwinding scan sequence - // with intervals. The only exception is the PendingPayableScanner and - // RetryPayableScanner, which are ever meant to run in a tight tandem. + // with intervals. The only exception is the PendingPayableScanner and retry- + // payable scanner, which are ever meant to run in a tight tandem. } } } @@ -913,7 +924,7 @@ impl Accountant { fn handle_request_of_scan_for_new_payable( &mut self, response_skeleton_opt: Option, - ) -> StartScanErrorResponse { + ) -> ScanRescheduleAfterEarlyStop { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_new_payable_scan_guarded( @@ -933,7 +944,7 @@ impl Accountant { .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - StartScanErrorResponse::DoNotSchedule + ScanRescheduleAfterEarlyStop::DoNotSchedule } Err(e) => self.handle_start_scan_error_and_prevent_scan_stall_point( PayableSequenceScanner::NewPayables, @@ -979,7 +990,7 @@ impl Accountant { fn handle_request_of_scan_for_pending_payable( &mut self, response_skeleton_opt: Option, - ) -> StartScanErrorResponse { + ) -> ScanRescheduleAfterEarlyStop { let result: Result = match self.consuming_wallet_opt.as_ref() { Some(consuming_wallet) => self.scanners.start_pending_payable_scan_guarded( @@ -992,14 +1003,14 @@ impl Accountant { None => Err(StartScanError::NoConsumingWalletFound), }; - let hint: StartScanErrorResponse = match result { + let hint: ScanRescheduleAfterEarlyStop = match result { Ok(scan_message) => { self.request_transaction_receipts_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) .expect("BlockchainBridge is dead"); - StartScanErrorResponse::DoNotSchedule + ScanRescheduleAfterEarlyStop::DoNotSchedule } Err(e) => { let initial_pending_payable_scan = self.scanners.initial_pending_payable_scan(); @@ -1025,7 +1036,7 @@ impl Accountant { scanner: PayableSequenceScanner, e: StartScanError, response_skeleton_opt: Option, - ) -> StartScanErrorResponse { + ) -> ScanRescheduleAfterEarlyStop { let is_externally_triggered = response_skeleton_opt.is_some(); e.log_error(&self.logger, scanner.into(), is_externally_triggered); @@ -1043,7 +1054,7 @@ impl Accountant { self.scan_schedulers .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error(scanner, &e, is_externally_triggered) + .resolve_rescheduling_on_error(scanner, &e, is_externally_triggered, &self.logger) } fn handle_request_of_scan_for_receivable( @@ -1215,7 +1226,7 @@ mod tests { use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp, CustomQuery}; use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::{MarkScanner, NewPayableScanDynIntervalComputerMock, ReplacementType, ScannerMock, ScannerReplacement}; + use crate::accountant::scanners::test_utils::{MarkScanner, NewPayableScanDynIntervalComputerMock, ReplacementType, RescheduleScanOnErrorResolverMock, ScannerMock, ScannerReplacement}; use crate::accountant::scanners::{StartScanError}; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, @@ -3251,7 +3262,7 @@ mod tests { System::current().stop(); system.run(); let flag_after = subject.scanners.initial_pending_payable_scan(); - assert_eq!(hint, StartScanErrorResponse::DoNotSchedule); + assert_eq!(hint, ScanRescheduleAfterEarlyStop::DoNotSchedule); assert_eq!(flag_before, true); assert_eq!(flag_after, false); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); @@ -3271,7 +3282,10 @@ mod tests { let hint = subject.handle_request_of_scan_for_pending_payable(None); let flag_after = subject.scanners.initial_pending_payable_scan(); - assert_eq!(hint, StartScanErrorResponse::Schedule(ScanType::Payables)); + assert_eq!( + hint, + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ); assert_eq!(flag_before, true); assert_eq!(flag_after, false); } @@ -3792,7 +3806,10 @@ mod tests { System::current().stop(); system.run(); - assert_eq!(result, StartScanErrorResponse::Schedule(ScanType::Payables)); + assert_eq!( + result, + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) + ); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recordings.len(), 0); } @@ -3872,6 +3889,31 @@ mod tests { ); } + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: Early stopped new payable scan \ + was suggested to be followed up by the scan for Receivables, which is not supported though" + )] + fn start_scan_early_stop_for_new_payables_requests_unexpected_receivable_scan_scheduling() { + let mut subject = AccountantBuilder::default().build(); + let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() + .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( + ScanType::Receivables, + )); + subject.scan_schedulers.reschedule_on_error_resolver = + Box::new(reschedule_on_error_resolver); + let system = System::new("test"); + let subject_addr = subject.start(); + + subject_addr + .try_send(ScanForNewPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + system.run(); + } + #[test] fn accountant_does_not_initiate_another_scan_if_one_is_already_running() { init_test_logging(); @@ -4023,6 +4065,73 @@ mod tests { log_handler.exists_log_containing("DEBUG: Accountant: Found 2 pending payables to process"); } + #[test] + fn start_scan_early_stop_for_pending_payables_if_initial_pending_payable_scan_and_no_wallet() { + let pending_payables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let new_payables_notify_params_arc = Arc::new(Mutex::new(vec![])); + let mut subject = AccountantBuilder::default().build(); + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(&pending_payables_notify_later_params_arc) + .stop_system_on_count_received(1), + ); + subject.scan_schedulers.pending_payable.interval = Duration::from_secs(60); + subject.scan_schedulers.payable.new_payable_notify = + Box::new(NotifyHandleMock::default().notify_params(&new_payables_notify_params_arc)); + let system = System::new("test"); + let subject_addr = subject.start(); + + subject_addr + .try_send(ScanForPendingPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + system.run(); + let pending_payables_notify_later_params = + pending_payables_notify_later_params_arc.lock().unwrap(); + assert_eq!( + *pending_payables_notify_later_params, + vec![( + ScanForPendingPayables { + response_skeleton_opt: None + }, + Duration::from_secs(60) + )] + ); + let new_payables_notify_params = new_payables_notify_params_arc.lock().unwrap(); + assert_eq!( + new_payables_notify_params.len(), + 0, + "Did not expect the new payables request" + ); + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: Early stopped pending payable scan \ + was suggested to be followed up by the scan for Receivables, which is not supported though" + )] + fn start_scan_early_stop_for_pending_payables_requests_unexpected_receivable_scan_scheduling() { + let mut subject = AccountantBuilder::default().build(); + let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() + .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( + ScanType::Receivables, + )); + subject.scan_schedulers.reschedule_on_error_resolver = + Box::new(reschedule_on_error_resolver); + let system = System::new("test"); + let subject_addr = subject.start(); + + subject_addr + .try_send(ScanForPendingPayables { + response_skeleton_opt: None, + }) + .unwrap(); + + system.run(); + } + #[test] fn report_routing_service_provided_message_is_received() { init_test_logging(); diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index abdc9bffa..4ee2af7c4 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -12,7 +12,7 @@ use crate::sub_lib::utils::{ use actix::{Actor, Context, Handler}; use masq_lib::logger::Logger; use masq_lib::messages::ScanType; -use std::fmt::Debug; +use std::fmt::{Debug, Display, Formatter}; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -44,11 +44,24 @@ pub enum PayableScanSchedulerError { } #[derive(Debug, PartialEq)] -pub enum StartScanErrorResponse { +pub enum ScanRescheduleAfterEarlyStop { Schedule(ScanType), DoNotSchedule, } +// impl Display for ScanRescheduleAfterEarlyStop { +// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +// match self { +// ScanRescheduleAfterEarlyStop::Schedule(scan_type) => { +// write!(f, "Schedule({:?})", scan_type) +// } +// ScanRescheduleAfterEarlyStop::DoNotSchedule => { +// write!(f, "DoNotSchedule") +// } +// } +// } +// } + #[derive(Debug, PartialEq, Clone, Copy)] pub enum PayableSequenceScanner { NewPayables, @@ -56,6 +69,16 @@ pub enum PayableSequenceScanner { PendingPayables { initial_pending_payable_scan: bool }, } +impl Display for PayableSequenceScanner { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PayableSequenceScanner::NewPayables => write!(f, "NewPayables"), + PayableSequenceScanner::RetryPayables => write!(f, "RetryPayables"), + PayableSequenceScanner::PendingPayables { .. } => write!(f, "PendingPayables"), + } + } +} + impl From for ScanType { fn from(scanner: PayableSequenceScanner) -> Self { match scanner { @@ -229,25 +252,27 @@ where // panic is justified only if the error was thought to be impossible by design and contextual // things but still happened. pub trait RescheduleScanOnErrorResolver { - fn resolve_rescheduling_for_given_error( + fn resolve_rescheduling_on_error( &self, scanner: PayableSequenceScanner, error: &StartScanError, is_externally_triggered: bool, - ) -> StartScanErrorResponse; + logger: &Logger, + ) -> ScanRescheduleAfterEarlyStop; } #[derive(Default)] pub struct RescheduleScanOnErrorResolverReal {} impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverReal { - fn resolve_rescheduling_for_given_error( + fn resolve_rescheduling_on_error( &self, scanner: PayableSequenceScanner, error: &StartScanError, is_externally_triggered: bool, - ) -> StartScanErrorResponse { - match scanner { + logger: &Logger, + ) -> ScanRescheduleAfterEarlyStop { + let reschedule_hint = match scanner { PayableSequenceScanner::NewPayables => { Self::resolve_new_payables(error, is_externally_triggered) } @@ -261,7 +286,25 @@ impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverReal { initial_pending_payable_scan, is_externally_triggered, ), - } + }; + + let scan_mode = if is_externally_triggered { + "manually requested" + } else { + "automatic" + }; + + info!( + logger, + "Rescheduling strategy '{:?}' was chosen after the {} {} scan encountered \ + the '{:?}' error", + reschedule_hint, + scan_mode, + scanner, + error + ); + + reschedule_hint } } @@ -269,16 +312,16 @@ impl RescheduleScanOnErrorResolverReal { fn resolve_new_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> StartScanErrorResponse { + ) -> ScanRescheduleAfterEarlyStop { if is_externally_triggered { - StartScanErrorResponse::DoNotSchedule + ScanRescheduleAfterEarlyStop::DoNotSchedule } else if matches!(err, StartScanError::ScanAlreadyRunning { .. }) { unreachable!( "an automatic scan of NewPayableScanner should never interfere with itself {:?}", err ) } else { - StartScanErrorResponse::Schedule(ScanType::Payables) + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) } } @@ -288,9 +331,9 @@ impl RescheduleScanOnErrorResolverReal { fn resolve_retry_payables( err: &StartScanError, is_externally_triggered: bool, - ) -> StartScanErrorResponse { + ) -> ScanRescheduleAfterEarlyStop { if is_externally_triggered { - StartScanErrorResponse::DoNotSchedule + ScanRescheduleAfterEarlyStop::DoNotSchedule } else { unreachable!( "{:?} should be impossible with RetryPayableScanner in automatic mode", @@ -303,12 +346,12 @@ impl RescheduleScanOnErrorResolverReal { err: &StartScanError, initial_pending_payable_scan: bool, is_externally_triggered: bool, - ) -> StartScanErrorResponse { + ) -> ScanRescheduleAfterEarlyStop { if is_externally_triggered { - StartScanErrorResponse::DoNotSchedule + ScanRescheduleAfterEarlyStop::DoNotSchedule } else if err == &StartScanError::NothingToProcess { if initial_pending_payable_scan { - StartScanErrorResponse::Schedule(ScanType::Payables) + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables) } else { unreachable!( "the automatic pending payable scan should always be requested only in need, \ @@ -324,7 +367,7 @@ impl RescheduleScanOnErrorResolverReal { // the user. // TODO Correctly, a check-point during the bootstrap should be the solution, // which wouldn't allow to come this far. - StartScanErrorResponse::Schedule(ScanType::PendingPayables) + ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables) } else { unreachable!( "PendingPayableScanner called later than the initial attempt, but \ @@ -344,13 +387,15 @@ impl RescheduleScanOnErrorResolverReal { mod tests { use crate::accountant::scanners::scan_schedulers::{ NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal, - PayableSequenceScanner, ScanSchedulers, StartScanErrorResponse, + PayableSequenceScanner, ScanRescheduleAfterEarlyStop, ScanSchedulers, }; use crate::accountant::scanners::{MTError, StartScanError}; use crate::sub_lib::accountant::ScanIntervals; use itertools::Itertools; use lazy_static::lazy_static; + use masq_lib::logger::Logger; use masq_lib::messages::ScanType; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -556,16 +601,20 @@ mod tests { } #[test] - fn resolve_rescheduling_for_given_error_works_for_pending_payables_if_externally_triggered() { + fn resolve_rescheduling_on_error_works_for_pending_payables_if_externally_triggered() { let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let test_name = + "resolve_rescheduling_on_error_works_for_pending_payables_if_externally_triggered"; test_what_if_externally_triggered( + &format!("{}(initial_pending_payable_scan = false)", test_name), &subject, PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: false, }, ); test_what_if_externally_triggered( + &format!("{}(initial_pending_payable_scan = true)", test_name), &subject, PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: true, @@ -574,25 +623,34 @@ mod tests { } fn test_what_if_externally_triggered( + test_name: &str, subject: &ScanSchedulers, scanner: PayableSequenceScanner, ) { + init_test_logging(); + let logger = Logger::new(test_name); + let test_log_handler = TestLogHandler::new(); ALL_START_SCAN_ERRORS .iter() .enumerate() .for_each(|(idx, error)| { let result = subject .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error(scanner, error, true); + .resolve_rescheduling_on_error(scanner, error, true, &logger); assert_eq!( result, - StartScanErrorResponse::DoNotSchedule, + ScanRescheduleAfterEarlyStop::DoNotSchedule, "We expected DoNotSchedule but got {:?} at idx {} for {:?}", result, idx, scanner ); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Rescheduling strategy 'DoNotSchedule' was chosen after \ + the manually requested {} scan encountered the '{:?}' error", + scanner, error + )); }) } @@ -600,23 +658,30 @@ mod tests { fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true( ) { let subject = ScanSchedulers::new(ScanIntervals::default(), true); + let test_name = "resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true"; + let logger = Logger::new(test_name); let result = subject .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error( + .resolve_rescheduling_on_error( PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: true, }, &StartScanError::NothingToProcess, false, + &logger, ); assert_eq!( result, - StartScanErrorResponse::Schedule(ScanType::Payables), + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got {:?}", result, ); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Rescheduling strategy 'Schedule(Payables)' was chosen after \ + the automatic PendingPayables scan encountered the 'NothingToProcess' error" + )); } #[test] @@ -631,17 +696,21 @@ mod tests { let _ = subject .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error( + .resolve_rescheduling_on_error( PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: false, }, &StartScanError::NothingToProcess, false, + &Logger::new("test"), ); } #[test] fn resolve_error_for_pending_p_if_no_consuming_wallet_found_in_initial_pending_payable_scan() { + init_test_logging(); + let test_name = "resolve_error_for_pending_p_if_no_consuming_wallet_found_in_initial_pending_payable_scan"; + let logger = Logger::new(test_name); let subject = ScanSchedulers::new(ScanIntervals::default(), true); let scanner = PayableSequenceScanner::PendingPayables { initial_pending_payable_scan: true, @@ -649,19 +718,24 @@ mod tests { let result = subject .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error( + .resolve_rescheduling_on_error( scanner, &StartScanError::NoConsumingWalletFound, false, + &logger, ); assert_eq!( result, - StartScanErrorResponse::Schedule(ScanType::PendingPayables), + ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables), "We expected Schedule(PendingPayables) but got {:?} for {:?}", result, scanner ); + TestLogHandler::new().exists_log_containing(&format!( + "INFO: {test_name}: Rescheduling strategy 'Schedule(PendingPayables)' was chosen after \ + the automatic PendingPayables scan encountered the 'NoConsumingWalletFound' error" + )); } #[test] @@ -678,10 +752,11 @@ mod tests { let _ = subject .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error( + .resolve_rescheduling_on_error( scanner, &StartScanError::NoConsumingWalletFound, false, + &Logger::new("test"), ); } @@ -696,12 +771,13 @@ mod tests { let panic = catch_unwind(AssertUnwindSafe(|| { subject .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error( + .resolve_rescheduling_on_error( PayableSequenceScanner::PendingPayables { initial_pending_payable_scan, }, *error, false, + &Logger::new("test"), ) })) .unwrap_err(); @@ -731,10 +807,16 @@ mod tests { } #[test] - fn resolve_rescheduling_for_given_error_works_for_retry_payables_if_externally_triggered() { + fn resolve_rescheduling_on_error_works_for_retry_payables_if_externally_triggered() { + let test_name = + "resolve_rescheduling_on_error_works_for_retry_payables_if_externally_triggered"; let subject = ScanSchedulers::new(ScanIntervals::default(), false); - test_what_if_externally_triggered(&subject, PayableSequenceScanner::RetryPayables {}); + test_what_if_externally_triggered( + test_name, + &subject, + PayableSequenceScanner::RetryPayables {}, + ); } #[test] @@ -745,10 +827,11 @@ mod tests { let panic = catch_unwind(AssertUnwindSafe(|| { subject .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error( + .resolve_rescheduling_on_error( PayableSequenceScanner::RetryPayables, error, false, + &Logger::new("test"), ) })) .unwrap_err(); @@ -768,10 +851,16 @@ mod tests { } #[test] - fn resolve_rescheduling_for_given_error_works_for_new_payables_if_externally_triggered() { + fn resolve_rescheduling_on_error_works_for_new_payables_if_externally_triggered() { + let test_name = + "resolve_rescheduling_on_error_works_for_new_payables_if_externally_triggered"; let subject = ScanSchedulers::new(ScanIntervals::default(), true); - test_what_if_externally_triggered(&subject, PayableSequenceScanner::NewPayables {}); + test_what_if_externally_triggered( + test_name, + &subject, + PayableSequenceScanner::NewPayables {}, + ); } #[test] @@ -784,41 +873,51 @@ mod tests { let _ = subject .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error( + .resolve_rescheduling_on_error( PayableSequenceScanner::NewPayables, &StartScanError::ScanAlreadyRunning { cross_scan_cause_opt: None, started_at: SystemTime::now(), }, false, + &Logger::new("test"), ); } #[test] - fn resolve_hint_for_new_payables_with_error_cases_resulting_in_future_rescheduling() { + fn resolve_new_payables_with_error_cases_resulting_in_future_rescheduling() { + let test_name = "resolve_new_payables_with_error_cases_resulting_in_future_rescheduling"; let inputs = ListOfStartScanErrors::default().eliminate_already_tested_variants(vec![ StartScanError::ScanAlreadyRunning { cross_scan_cause_opt: None, started_at: SystemTime::now(), }, ]); + let logger = Logger::new(test_name); + let test_log_handler = TestLogHandler::new(); let subject = ScanSchedulers::new(ScanIntervals::default(), true); inputs.errors.iter().for_each(|error| { let result = subject .reschedule_on_error_resolver - .resolve_rescheduling_for_given_error( + .resolve_rescheduling_on_error( PayableSequenceScanner::NewPayables, *error, false, + &logger, ); assert_eq!( result, - StartScanErrorResponse::Schedule(ScanType::Payables), + ScanRescheduleAfterEarlyStop::Schedule(ScanType::Payables), "We expected Schedule(Payables) but got '{:?}'", result, - ) + ); + test_log_handler.exists_log_containing(&format!( + "INFO: {test_name}: Rescheduling strategy 'Schedule(Payables)' was chosen after \ + the automatic NewPayables scan encountered the '{:?}' error", + error + )); }) } diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 963702ed3..26aa15dc3 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -8,7 +8,10 @@ use crate::accountant::scanners::payable_scanner_extension::msgs::{ use crate::accountant::scanners::payable_scanner_extension::{ MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, }; -use crate::accountant::scanners::scan_schedulers::NewPayableScanDynIntervalComputer; +use crate::accountant::scanners::scan_schedulers::{ + NewPayableScanDynIntervalComputer, PayableSequenceScanner, RescheduleScanOnErrorResolver, + ScanRescheduleAfterEarlyStop, +}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableScanResult; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanResult; use crate::accountant::scanners::{ @@ -436,3 +439,52 @@ pub fn assert_timestamps_from_str(examined_str: &str, expected_timestamps: Vec>>, + resolve_rescheduling_on_error_results: RefCell>, +} + +impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverMock { + fn resolve_rescheduling_on_error( + &self, + scanner: PayableSequenceScanner, + error: &StartScanError, + is_externally_triggered: bool, + logger: &Logger, + ) -> ScanRescheduleAfterEarlyStop { + self.resolve_rescheduling_on_error_params + .lock() + .unwrap() + .push(( + scanner, + error.clone(), + is_externally_triggered, + logger.clone(), + )); + self.resolve_rescheduling_on_error_results + .borrow_mut() + .remove(0) + } +} + +impl RescheduleScanOnErrorResolverMock { + pub fn resolve_rescheduling_on_error_params( + mut self, + params: &Arc>>, + ) -> Self { + self.resolve_rescheduling_on_error_params = params.clone(); + self + } + pub fn resolve_rescheduling_on_error_result( + self, + result: ScanRescheduleAfterEarlyStop, + ) -> Self { + self.resolve_rescheduling_on_error_results + .borrow_mut() + .push(result); + self + } +} From 5da5f4781ed8f8d7c0de7b49af288ea8b43dc0e5 Mon Sep 17 00:00:00 2001 From: Bert Date: Sat, 7 Jun 2025 02:20:55 +0200 Subject: [PATCH 45/49] GH-602: removed commented out impl --- node/src/accountant/scanners/scan_schedulers.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 4ee2af7c4..546123bd1 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -49,19 +49,6 @@ pub enum ScanRescheduleAfterEarlyStop { DoNotSchedule, } -// impl Display for ScanRescheduleAfterEarlyStop { -// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { -// match self { -// ScanRescheduleAfterEarlyStop::Schedule(scan_type) => { -// write!(f, "Schedule({:?})", scan_type) -// } -// ScanRescheduleAfterEarlyStop::DoNotSchedule => { -// write!(f, "DoNotSchedule") -// } -// } -// } -// } - #[derive(Debug, PartialEq, Clone, Copy)] pub enum PayableSequenceScanner { NewPayables, From 2d3a16d6de1f61871eb0cddc949e614fc749f6f2 Mon Sep 17 00:00:00 2001 From: Bert Date: Thu, 12 Jun 2025 23:45:53 +0200 Subject: [PATCH 46/49] GH-602: vast majority of the review is covered --- node/src/accountant/mod.rs | 119 ++++++++++++------ node/src/accountant/scanners/mod.rs | 20 +-- .../accountant/scanners/scan_schedulers.rs | 61 +++++---- node/src/test_utils/recorder.rs | 17 +-- node/src/test_utils/recorder_counter_msgs.rs | 2 +- 5 files changed, 133 insertions(+), 86 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index ea4899697..58d817442 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1227,7 +1227,7 @@ mod tests { use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; use crate::accountant::scanners::test_utils::{MarkScanner, NewPayableScanDynIntervalComputerMock, ReplacementType, RescheduleScanOnErrorResolverMock, ScannerMock, ScannerReplacement}; - use crate::accountant::scanners::{StartScanError}; + use crate::accountant::scanners::{PendingPayableScanner, StartScanError}; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; @@ -2608,12 +2608,11 @@ mod tests { } #[test] - fn accountant_scans_after_startup_and_does_not_detect_straggled_pending_payables() { - // We do ensure the PendingPayableScanner runs before the NewPayableScanner. Not interested - // in an exact placing of the ReceivableScanner too much. + fn accountant_scans_after_startup_and_does_not_detect_any_pending_payables() { + // We will want to prove that the PendingPayableScanner runs before the NewPayableScanner. + // Their relationship towards the ReceivableScanner isn't important. init_test_logging(); - let test_name = - "accountant_scans_after_startup_and_does_not_detect_straggled_pending_payables"; + let test_name = "accountant_scans_after_startup_and_does_not_detect_any_pending_payables"; let pending_payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); let receivable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); let payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); @@ -2662,19 +2661,25 @@ mod tests { send_start_message!(subject_subs); // The system is stopped by the NotifyLaterHandleMock for the Receivable scanner + let before = SystemTime::now(); system.run(); - let pp_scan_started_at = assert_pending_payable_scanner_for_no_pending_payable_found( + let after = SystemTime::now(); + assert_pending_payable_scanner_for_no_pending_payable_found( test_name, consuming_wallet, pending_payable_start_scan_params_arc, scan_for_pending_payables_notify_later_params_arc, + before, + after, ); - let p_scheduling_happened_at = assert_payable_scanner_for_no_pending_payable_found( + assert_payable_scanner_for_no_pending_payable_found( scan_for_new_payables_notify_later_params_arc, new_payable_expected_computed_interval, scan_for_new_payables_notify_params_arc, scan_for_retry_payables_notify_params_arc, compute_interval_params_arc, + before, + after, ); assert_receivable_scanner( test_name, @@ -2683,21 +2688,25 @@ mod tests { receivable_scan_interval, scan_for_receivables_notify_later_params_arc, ); - // We expect the PendingPayableScanner to take place before the PayableScanner. - // I do believe it's impossible for these two events to happen within the same nanosecond, - // until somebody fiddles with MASQ tests on a true supercomputer. - assert!( - pp_scan_started_at < p_scheduling_happened_at, - "We failed to prove that the PendingPayableScanner runs before the PayableScanner." - ); + // The test lays down evidences that the NewPayableScanner couldn't run before + // the PendingPayableScanner, which is an intention. + // To interpret the evidence, we have to notice that the PendingPayableScanner ran + // certainly, while it wasn't attempted to schedule in the whole test. That points out that + // the scanning sequence started spontaneously, not requiring any prior scheduling. Most + // importantly, regarding the payable scanner, it ran not even once. We know, though, + // that its scheduling did take place, specifically an urgent call of the new payable mode. + // That totally corresponds with the expected behavior where the PendingPayableScanner + // should first search for any stray pending payables; if no findings, the NewPayableScanner + // is supposed to go next, and it shouldn't have to undertake the standard new-payable + // interval, but here, at the beginning, it comes immediately. } #[test] - fn accountant_scans_after_startup_and_detects_straggled_pending_payable() { + fn accountant_scans_after_startup_and_detects_pending_payable_from_before() { // We do ensure the PendingPayableScanner runs before the NewPayableScanner. Not interested - // in an exact placing of the ReceivableScanner too much. + // in an exact placing of the ReceivableScanner so much. init_test_logging(); - let test_name = "accountant_scans_after_startup_and_detects_straggled_pending_payable"; + let test_name = "accountant_scans_after_startup_and_detects_pending_payable_from_before"; let pending_payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); let pending_payable_finish_scan_params_arc = Arc::new(Mutex::new(vec![])); let receivable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); @@ -2797,7 +2806,9 @@ mod tests { send_start_message!(subject_subs); // The system is stopped by the NotifyHandleLaterMock for the PendingPayable scanner + let before = SystemTime::now(); system.run(); + let after = SystemTime::now(); assert_pending_payable_scanner_for_some_pending_payable_found( test_name, consuming_wallet.clone(), @@ -2806,6 +2817,8 @@ mod tests { scan_for_pending_payables_notify_later_params_arc, pending_payable_expected_notify_later_interval, expected_report_transaction_receipts, + before, + after, ); assert_payable_scanner_for_some_pending_payable_found( test_name, @@ -2824,9 +2837,9 @@ mod tests { receivable_scan_interval, scan_for_receivables_notify_later_params_arc, ); - // Given the assertions proving that the pending payable scanner will run multiple times - // before the new payable scanner runs at least once (even not scheduled yet), its front - // position is clear + // Given the assertions prove that the pending payable scanner has run multiple times + // before the new payable scanner started or was scheduled, the front position belongs to + // the one first mentioned, no doubts. } fn set_up_subject_for_no_pending_payables_found_startup_test( @@ -2994,24 +3007,29 @@ mod tests { scan_for_pending_payables_notify_later_params_arc: Arc< Mutex>, >, - ) -> SystemTime { - let (pp_scan_started_at, pp_logger) = - pending_payable_common(consuming_wallet, pending_payable_start_scan_params_arc); + before_performing_act: SystemTime, + after_performing_act: SystemTime, + ) { + let pp_logger = pending_payable_common( + consuming_wallet, + pending_payable_start_scan_params_arc, + before_performing_act, + after_performing_act, + ); let scan_for_pending_payables_notify_later_params = scan_for_pending_payables_notify_later_params_arc .lock() .unwrap(); - // The part of running the `NewPayableScanner` is deliberately omitted here, we stop - // the test right before that. Normally, the first occasion for scheduling - // the `PendingPayableScanner` would've lied no sooner than after the `NewPayableScan` - // finishes, having produced at least one blockchain transactions. + // We stop the test right before running the `NewPayableScanner` and so that part is + // missing. We cannot capture the first occasion of scheduling the `PendingPayableScanner` + // any sooner than straight after the `NewPayableScan` finishes, if only it produced at + // least one blockchain transaction. assert!( scan_for_pending_payables_notify_later_params.is_empty(), "We did not expect to see another schedule for pending payables, but it happened {:?}", scan_for_pending_payables_notify_later_params ); assert_using_the_same_logger(&pp_logger, test_name, Some("pp")); - pp_scan_started_at } fn assert_pending_payable_scanner_for_some_pending_payable_found( @@ -3028,9 +3046,15 @@ mod tests { >, pending_payable_expected_notify_later_interval: Duration, expected_report_tx_receipts_msg: ReportTransactionReceipts, + before_perforrming_act: SystemTime, + after_perforrming_act: SystemTime, ) { - let (_, pp_start_scan_logger) = - pending_payable_common(consuming_wallet, pending_payable_start_scan_params_arc); + let pp_start_scan_logger = pending_payable_common( + consuming_wallet, + pending_payable_start_scan_params_arc, + before_perforrming_act, + after_perforrming_act, + ); assert_using_the_same_logger(&pp_start_scan_logger, test_name, Some("pp start scan")); let mut pending_payable_finish_scan_params = pending_payable_finish_scan_params_arc.lock().unwrap(); @@ -3064,7 +3088,9 @@ mod tests { pending_payable_start_scan_params_arc: Arc< Mutex, Logger, String)>>, >, - ) -> (SystemTime, Logger) { + before_performing_act: SystemTime, + after_performing_act: SystemTime, + ) -> Logger { let mut pending_payable_params = pending_payable_start_scan_params_arc.lock().unwrap(); let ( pp_wallet, @@ -3085,7 +3111,15 @@ mod tests { "Should be empty but was {:?}", pending_payable_params ); - (pp_scan_started_at, pp_logger) + assert!( + before_performing_act <= pp_scan_started_at + && pp_scan_started_at <= after_performing_act, + "The scanner was supposed to run between {:?} and {:?} but it was {:?}", + before_performing_act, + after_performing_act, + pp_scan_started_at + ); + pp_logger } fn assert_payable_scanner_for_no_pending_payable_found( @@ -3096,8 +3130,10 @@ mod tests { scan_for_new_payables_notify_params_arc: Arc>>, scan_for_retry_payables_notify_params_arc: Arc>>, compute_interval_params_arc: Arc>>, - ) -> SystemTime { - // First, there is no functionality from the payable scanner actually running. + before_performing_act: SystemTime, + after_performing_act: SystemTime, + ) { + // Note that there is no functionality from the payable scanner actually running. // We only witness it to be scheduled. let scan_for_new_payables_notify_later_params = scan_for_new_payables_notify_later_params_arc @@ -3130,7 +3166,7 @@ mod tests { "We did not expect any scheduling of retry payables, but it happened {:?}", scan_for_retry_payables_notify_params ); - p_scheduling_now + assert!(before_performing_act <= p_scheduling_now && p_scheduling_now <= after_performing_act, "The payable scan scheduling was supposed to take place between {:?} and {:?} but it was {:?}", before_performing_act, after_performing_act, p_scheduling_now); } fn assert_payable_scanner_for_some_pending_payable_found( @@ -3266,7 +3302,7 @@ mod tests { assert_eq!(flag_before, true); assert_eq!(flag_after, false); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); - let _recorded_msg = blockchain_bridge_recording.get_record::(0); + let _ = blockchain_bridge_recording.get_record::(0); } #[test] @@ -3894,7 +3930,7 @@ mod tests { expected = "internal error: entered unreachable code: Early stopped new payable scan \ was suggested to be followed up by the scan for Receivables, which is not supported though" )] - fn start_scan_early_stop_for_new_payables_requests_unexpected_receivable_scan_scheduling() { + fn start_scan_error_in_new_payables_and_unexpected_reaction_by_receivable_scan_scheduling() { let mut subject = AccountantBuilder::default().build(); let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( @@ -4066,10 +4102,12 @@ mod tests { } #[test] - fn start_scan_early_stop_for_pending_payables_if_initial_pending_payable_scan_and_no_wallet() { + fn start_scan_error_in_pending_payables_if_initial_scan_is_true_and_no_consuming_wallet_found() + { let pending_payables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let new_payables_notify_params_arc = Arc::new(Mutex::new(vec![])); let mut subject = AccountantBuilder::default().build(); + subject.consuming_wallet_opt = None; subject.scan_schedulers.pending_payable.handle = Box::new( NotifyLaterHandleMock::default() .notify_later_params(&pending_payables_notify_later_params_arc) @@ -4112,7 +4150,8 @@ mod tests { expected = "internal error: entered unreachable code: Early stopped pending payable scan \ was suggested to be followed up by the scan for Receivables, which is not supported though" )] - fn start_scan_early_stop_for_pending_payables_requests_unexpected_receivable_scan_scheduling() { + fn start_scan_error_in_pending_payables_and_unexpected_reaction_by_receivable_scan_scheduling() + { let mut subject = AccountantBuilder::default().build(); let reschedule_on_error_resolver = RescheduleScanOnErrorResolverMock::default() .resolve_rescheduling_on_error_result(ScanRescheduleAfterEarlyStop::Schedule( diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 27d6082fb..ca1810290 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -4332,19 +4332,19 @@ mod tests { |( err, form_expected_log_msg, - severity_for_externally_triggered_scans, - severity_for_automatic_scans, + log_severity_for_externally_triggered_scans, + log_severity_for_automatic_scans, )| { - err.log_error(&logger, ScanType::Payables, true); + let test_log_error_by_mode = + |is_externally_triggered: bool, expected_severity: &str| { + err.log_error(&logger, ScanType::Payables, is_externally_triggered); + let expected_log_msg = form_expected_log_msg(expected_severity); + test_log_handler.exists_log_containing(&expected_log_msg); + }; - let expected_log_msg = - form_expected_log_msg(severity_for_externally_triggered_scans); - test_log_handler.exists_log_containing(&expected_log_msg); + test_log_error_by_mode(true, log_severity_for_externally_triggered_scans); - err.log_error(&logger, ScanType::Payables, false); - - let expected_log_msg = form_expected_log_msg(severity_for_automatic_scans); - test_log_handler.exists_log_containing(&expected_log_msg); + test_log_error_by_mode(false, log_severity_for_automatic_scans); }, ); } diff --git a/node/src/accountant/scanners/scan_schedulers.rs b/node/src/accountant/scanners/scan_schedulers.rs index 546123bd1..77ac6646e 100644 --- a/node/src/accountant/scanners/scan_schedulers.rs +++ b/node/src/accountant/scanners/scan_schedulers.rs @@ -275,21 +275,7 @@ impl RescheduleScanOnErrorResolver for RescheduleScanOnErrorResolverReal { ), }; - let scan_mode = if is_externally_triggered { - "manually requested" - } else { - "automatic" - }; - - info!( - logger, - "Rescheduling strategy '{:?}' was chosen after the {} {} scan encountered \ - the '{:?}' error", - reschedule_hint, - scan_mode, - scanner, - error - ); + Self::log_rescheduling(scanner, is_externally_triggered, logger, &reschedule_hint); reschedule_hint } @@ -352,8 +338,8 @@ impl RescheduleScanOnErrorResolverReal { // StartScanError::NothingToProcess can be evaluated); but may be cautious and // prevent starting the NewPayableScanner. Repeating this scan endlessly may alarm // the user. - // TODO Correctly, a check-point during the bootstrap should be the solution, - // which wouldn't allow to come this far. + // TODO Correctly, a check-point during the bootstrap that wouldn't allow to come + // this far should be the solution. Part of the issue mentioned in GH-799 ScanRescheduleAfterEarlyStop::Schedule(ScanType::PendingPayables) } else { unreachable!( @@ -368,6 +354,27 @@ impl RescheduleScanOnErrorResolverReal { ) } } + + fn log_rescheduling( + scanner: PayableSequenceScanner, + is_externally_triggered: bool, + logger: &Logger, + reschedule_hint: &ScanRescheduleAfterEarlyStop, + ) { + let scan_mode = if is_externally_triggered { + "Manual" + } else { + "Automatic" + }; + + debug!( + logger, + "{} {} scan failed - rescheduling strategy: \"{:?}\"", + scan_mode, + scanner, + reschedule_hint + ); + } } #[cfg(test)] @@ -634,9 +641,9 @@ mod tests { scanner ); test_log_handler.exists_log_containing(&format!( - "INFO: {test_name}: Rescheduling strategy 'DoNotSchedule' was chosen after \ - the manually requested {} scan encountered the '{:?}' error", - scanner, error + "DEBUG: {test_name}: Manual {} scan failed - rescheduling strategy: \ + \"DoNotSchedule\"", + scanner )); }) } @@ -644,6 +651,7 @@ mod tests { #[test] fn resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true( ) { + init_test_logging(); let subject = ScanSchedulers::new(ScanIntervals::default(), true); let test_name = "resolve_error_for_pending_payables_if_nothing_to_process_and_initial_pending_payable_scan_true"; let logger = Logger::new(test_name); @@ -666,8 +674,8 @@ mod tests { result, ); TestLogHandler::new().exists_log_containing(&format!( - "INFO: {test_name}: Rescheduling strategy 'Schedule(Payables)' was chosen after \ - the automatic PendingPayables scan encountered the 'NothingToProcess' error" + "DEBUG: {test_name}: Automatic PendingPayables scan failed - rescheduling strategy: \ + \"Schedule(Payables)\"" )); } @@ -720,8 +728,8 @@ mod tests { scanner ); TestLogHandler::new().exists_log_containing(&format!( - "INFO: {test_name}: Rescheduling strategy 'Schedule(PendingPayables)' was chosen after \ - the automatic PendingPayables scan encountered the 'NoConsumingWalletFound' error" + "DEBUG: {test_name}: Automatic PendingPayables scan failed - rescheduling strategy: \ + \"Schedule(PendingPayables)\"" )); } @@ -901,9 +909,8 @@ mod tests { result, ); test_log_handler.exists_log_containing(&format!( - "INFO: {test_name}: Rescheduling strategy 'Schedule(Payables)' was chosen after \ - the automatic NewPayables scan encountered the '{:?}' error", - error + "DEBUG: {test_name}: Automatic NewPayables scan failed - rescheduling strategy: \ + \"Schedule(Payables)\"", )); }) } diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index 33c023197..2636ae3b9 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -706,7 +706,6 @@ mod tests { use crate::blockchain::blockchain_bridge::BlockchainBridge; use crate::sub_lib::neighborhood::{ConfigChange, Hops, WalletPair}; use crate::test_utils::make_wallet; - use crate::test_utils::neighborhood_test_utils::make_ip; use crate::test_utils::recorder_counter_msgs::SendableCounterMsgWithRecipient; use crate::{ match_lazily_every_type_id, setup_for_counter_msg_triggered_via_specific_msg_id_method, @@ -845,10 +844,10 @@ mod tests { // Case two let cm_setup_2 = { let counter_msg_strayed = StartMessage {}; - let random_id = TypeId::of::(); - let id_method = MsgIdentification::ByType(random_id); + let screwed_id = TypeId::of::(); + let id_method = MsgIdentification::ByType(screwed_id); SingleTypeCounterMsgSetup::new( - random_id, + screwed_id, id_method, vec![Box::new(SendableCounterMsgWithRecipient::new( counter_msg_strayed, @@ -871,7 +870,9 @@ mod tests { .tmb(0), }; let id_method = MsgIdentification::ByMatch { - exemplar: Box::new(NewPublicIp { new_ip: make_ip(1) }), + exemplar: Box::new(NewPublicIp { + new_ip: IpAddr::V4(Ipv4Addr::new(7, 6, 5, 4)), + }), }; ( trigger_msg, @@ -888,7 +889,7 @@ mod tests { // Case four let (trigger_msg_4_matching, cm_setup_4, counter_msg_4) = { let trigger_msg = NewPublicIp { - new_ip: IpAddr::V4(Ipv4Addr::new(4, 5, 6, 7)), + new_ip: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), }; let msg_type_id = trigger_msg.type_id(); let counter_msg = NodeToUiMessage { @@ -919,7 +920,7 @@ mod tests { let (subject, _, subject_recording_arc) = make_recorder(); let subject_addr = subject.start(); // Supplying messages deliberately in a tangled manner to express that the mechanism is - // robust enough to compensate it + // robust enough to compensate for it subject_addr .try_send(SetUpCounterMsgs { setups: vec![cm_setup_3, cm_setup_1, cm_setup_2, cm_setup_4], @@ -935,7 +936,7 @@ mod tests { .unwrap(); system.run(); - // Actual counter messages that flew over in this test + // Actual counter-messages that flew over in this test let respondent_recording = respondent_recording_arc.lock().unwrap(); let _first_counter_msg_recorded = respondent_recording.get_record::(0); let second_counter_msg_recorded = respondent_recording.get_record::(1); diff --git a/node/src/test_utils/recorder_counter_msgs.rs b/node/src/test_utils/recorder_counter_msgs.rs index d0c86b9d0..f2cef1520 100644 --- a/node/src/test_utils/recorder_counter_msgs.rs +++ b/node/src/test_utils/recorder_counter_msgs.rs @@ -124,7 +124,7 @@ impl CounterMessages { } } -// Note that you're not limited to triggering an only message a time, but you can supply more +// Note that you're not limited to triggering only one message at a time, but you can supply more // messages to this macro, all triggered by the same type id. #[macro_export] macro_rules! setup_for_counter_msg_triggered_via_type_id{ From a5a17d1d8027f0c085e4c403e746a0557fd79eeb Mon Sep 17 00:00:00 2001 From: Bert Date: Fri, 13 Jun 2025 00:57:26 +0200 Subject: [PATCH 47/49] GH-602: refactoring in the biggeste tests - a la utkarsh --- node/src/accountant/mod.rs | 404 ++++++++++++++++++------------------- 1 file changed, 196 insertions(+), 208 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 58d817442..bb6fc3f81 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1227,7 +1227,7 @@ mod tests { use crate::accountant::payment_adjuster::Adjustment; use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; use crate::accountant::scanners::test_utils::{MarkScanner, NewPayableScanDynIntervalComputerMock, ReplacementType, RescheduleScanOnErrorResolverMock, ScannerMock, ScannerReplacement}; - use crate::accountant::scanners::{PendingPayableScanner, StartScanError}; + use crate::accountant::scanners::{StartScanError}; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; @@ -2613,41 +2613,31 @@ mod tests { // Their relationship towards the ReceivableScanner isn't important. init_test_logging(); let test_name = "accountant_scans_after_startup_and_does_not_detect_any_pending_payables"; - let pending_payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); - let receivable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); - let payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); - let scan_for_pending_payables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); - let scan_for_new_payables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); - let scan_for_new_payables_notify_params_arc = Arc::new(Mutex::new(vec![])); - let scan_for_retry_payables_notify_params_arc = Arc::new(Mutex::new(vec![])); + let scan_params = ScanParams::default(); + let notify_and_notify_later_params = NotifyAndNotifyLaterParams::default(); let compute_interval_params_arc = Arc::new(Mutex::new(vec![])); - let scan_for_receivables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); let earning_wallet = make_wallet("earning"); let consuming_wallet = make_wallet("consuming"); let system = System::new(test_name); let _ = SystemKillerActor::new(Duration::from_secs(10)).start(); let config = bc_from_wallets(consuming_wallet.clone(), earning_wallet.clone()); - let pending_payable_scanner = ScannerMock::new() + let payable_scanner = ScannerMock::new() .scan_started_at_result(None) - .start_scan_params(&pending_payable_start_scan_params_arc) + .start_scan_params(&scan_params.payable_start_scan) .start_scan_result(Err(StartScanError::NothingToProcess)); - let receivable_scanner = ScannerMock::new() + let pending_payable_scanner = ScannerMock::new() .scan_started_at_result(None) - .start_scan_params(&receivable_start_scan_params_arc) + .start_scan_params(&scan_params.pending_payable_start_scan) .start_scan_result(Err(StartScanError::NothingToProcess)); - let payable_scanner = ScannerMock::new() + let receivable_scanner = ScannerMock::new() .scan_started_at_result(None) - .start_scan_params(&payable_start_scan_params_arc) + .start_scan_params(&scan_params.receivable_start_scan) .start_scan_result(Err(StartScanError::NothingToProcess)); let (subject, new_payable_expected_computed_interval, receivable_scan_interval) = set_up_subject_for_no_pending_payables_found_startup_test( test_name, - &scan_for_pending_payables_notify_later_params_arc, - &scan_for_new_payables_notify_later_params_arc, - &scan_for_new_payables_notify_params_arc, - &scan_for_retry_payables_notify_params_arc, + ¬ify_and_notify_later_params, &compute_interval_params_arc, - &scan_for_receivables_notify_later_params_arc, config, pending_payable_scanner, receivable_scanner, @@ -2667,26 +2657,24 @@ mod tests { assert_pending_payable_scanner_for_no_pending_payable_found( test_name, consuming_wallet, - pending_payable_start_scan_params_arc, - scan_for_pending_payables_notify_later_params_arc, + &scan_params.pending_payable_start_scan, + ¬ify_and_notify_later_params.pending_payables_notify_later, before, after, ); assert_payable_scanner_for_no_pending_payable_found( - scan_for_new_payables_notify_later_params_arc, - new_payable_expected_computed_interval, - scan_for_new_payables_notify_params_arc, - scan_for_retry_payables_notify_params_arc, + ¬ify_and_notify_later_params, compute_interval_params_arc, + new_payable_expected_computed_interval, before, after, ); assert_receivable_scanner( test_name, earning_wallet, - receivable_start_scan_params_arc, + &scan_params.receivable_start_scan, + ¬ify_and_notify_later_params.receivables_notify_later, receivable_scan_interval, - scan_for_receivables_notify_later_params_arc, ); // The test lays down evidences that the NewPayableScanner couldn't run before // the PendingPayableScanner, which is an intention. @@ -2707,62 +2695,50 @@ mod tests { // in an exact placing of the ReceivableScanner so much. init_test_logging(); let test_name = "accountant_scans_after_startup_and_detects_pending_payable_from_before"; - let pending_payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); - let pending_payable_finish_scan_params_arc = Arc::new(Mutex::new(vec![])); - let receivable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); - let payable_start_scan_params_arc = Arc::new(Mutex::new(vec![])); - let payable_finish_scan_params_arc = Arc::new(Mutex::new(vec![])); - let scan_for_pending_payables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); - let scan_for_new_payables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); - let scan_for_new_payables_notify_params_arc = Arc::new(Mutex::new(vec![])); - let scan_for_retry_payables_notify_params_arc = Arc::new(Mutex::new(vec![])); - let scan_for_receivables_notify_later_params_arc = Arc::new(Mutex::new(vec![])); + let scan_params = ScanParams::default(); + let notify_and_notify_later_params = NotifyAndNotifyLaterParams::default(); let earning_wallet = make_wallet("earning"); let consuming_wallet = make_wallet("consuming"); let system = System::new(test_name); let _ = SystemKillerActor::new(Duration::from_secs(10)).start(); let config = bc_from_wallets(consuming_wallet.clone(), earning_wallet.clone()); let pp_fingerprint = make_pending_payable_fingerprint(); - let pending_payable_scanner = ScannerMock::new() - .scan_started_at_result(None) - .start_scan_params(&pending_payable_start_scan_params_arc) - .start_scan_result(Ok(RequestTransactionReceipts { - pending_payable_fingerprints: vec![pp_fingerprint], - response_skeleton_opt: None, - })) - .finish_scan_params(&pending_payable_finish_scan_params_arc) - .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); - let receivable_scanner = ScannerMock::new() - .scan_started_at_result(None) - .start_scan_params(&receivable_start_scan_params_arc) - .start_scan_result(Err(StartScanError::NothingToProcess)); let payable_scanner = ScannerMock::new() .scan_started_at_result(None) .scan_started_at_result(None) - .start_scan_params(&payable_start_scan_params_arc) + .start_scan_params(&scan_params.payable_start_scan) .start_scan_result(Ok(QualifiedPayablesMessage { qualified_payables: vec![make_payable_account(123)], consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: None, })) - .finish_scan_params(&payable_finish_scan_params_arc) + .finish_scan_params(&scan_params.payable_finish_scan) // Important .finish_scan_result(PayableScanResult { ui_response_opt: None, result: OperationOutcome::NewPendingPayable, }); + let pending_payable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&scan_params.pending_payable_start_scan) + .start_scan_result(Ok(RequestTransactionReceipts { + pending_payable_fingerprints: vec![pp_fingerprint], + response_skeleton_opt: None, + })) + .finish_scan_params(&scan_params.pending_payable_finish_scan) + .finish_scan_result(PendingPayableScanResult::PaymentRetryRequired); + let receivable_scanner = ScannerMock::new() + .scan_started_at_result(None) + .start_scan_params(&scan_params.receivable_start_scan) + .start_scan_result(Err(StartScanError::NothingToProcess)); let (subject, pending_payable_expected_notify_later_interval, receivable_scan_interval) = set_up_subject_for_some_pending_payable_found_startup_test( test_name, - &scan_for_pending_payables_notify_later_params_arc, - &scan_for_new_payables_notify_later_params_arc, - &scan_for_new_payables_notify_params_arc, - &scan_for_retry_payables_notify_params_arc, - &scan_for_receivables_notify_later_params_arc, + ¬ify_and_notify_later_params, config, + payable_scanner, pending_payable_scanner, receivable_scanner, - payable_scanner, ); let (peer_actors, addresses) = peer_actors_builder().build_and_provide_addresses(); let subject_addr: Addr = subject.start(); @@ -2812,9 +2788,8 @@ mod tests { assert_pending_payable_scanner_for_some_pending_payable_found( test_name, consuming_wallet.clone(), - pending_payable_start_scan_params_arc, - pending_payable_finish_scan_params_arc, - scan_for_pending_payables_notify_later_params_arc, + &scan_params, + ¬ify_and_notify_later_params.pending_payables_notify_later, pending_payable_expected_notify_later_interval, expected_report_transaction_receipts, before, @@ -2823,39 +2798,48 @@ mod tests { assert_payable_scanner_for_some_pending_payable_found( test_name, consuming_wallet, + &scan_params, + ¬ify_and_notify_later_params, expected_sent_payables, - payable_finish_scan_params_arc, - payable_start_scan_params_arc, - scan_for_new_payables_notify_later_params_arc, - scan_for_new_payables_notify_params_arc, - scan_for_retry_payables_notify_params_arc, ); assert_receivable_scanner( test_name, earning_wallet, - receivable_start_scan_params_arc, + &scan_params.receivable_start_scan, + ¬ify_and_notify_later_params.receivables_notify_later, receivable_scan_interval, - scan_for_receivables_notify_later_params_arc, ); // Given the assertions prove that the pending payable scanner has run multiple times // before the new payable scanner started or was scheduled, the front position belongs to // the one first mentioned, no doubts. } + #[derive(Default)] + struct ScanParams { + payable_start_scan: + Arc, Logger, String)>>>, + payable_finish_scan: Arc>>, + pending_payable_start_scan: + Arc, Logger, String)>>>, + pending_payable_finish_scan: Arc>>, + receivable_start_scan: + Arc, Logger, String)>>>, + // receivable_finish_scan ... not needed + } + + #[derive(Default)] + struct NotifyAndNotifyLaterParams { + new_payables_notify_later: Arc>>, + new_payables_notify: Arc>>, + retry_payables_notify: Arc>>, + pending_payables_notify_later: Arc>>, + receivables_notify_later: Arc>>, + } + fn set_up_subject_for_no_pending_payables_found_startup_test( test_name: &str, - scan_for_pending_payables_notify_later_params_arc: &Arc< - Mutex>, - >, - scan_for_new_payables_notify_later_params_arc: &Arc< - Mutex>, - >, - scan_for_new_payables_notify_params_arc: &Arc>>, - scan_for_retry_payables_notify_params_arc: &Arc>>, + notify_and_notify_later_params: &NotifyAndNotifyLaterParams, compute_interval_params_arc: &Arc>>, - scan_for_receivables_notify_later_params_arc: &Arc< - Mutex>, - >, config: BootstrapperConfig, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, @@ -2869,42 +2853,32 @@ mod tests { >, payable_scanner: ScannerMock, ) -> (Accountant, Duration, Duration) { - let mut subject = AccountantBuilder::default() - .logger(Logger::new(test_name)) - .bootstrapper_config(config) - .build(); - subject - .scanners - .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( - pending_payable_scanner, - ))); - subject - .scanners - .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Mock( - receivable_scanner, - ))); - subject - .scanners - .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( - payable_scanner, - ))); - let pending_payable_notify_later_handle_mock = NotifyLaterHandleMock::default() - .notify_later_params(&scan_for_pending_payables_notify_later_params_arc); - subject.scan_schedulers.pending_payable.handle = - Box::new(pending_payable_notify_later_handle_mock); + let mut subject = make_subject_and_inject_scanners( + test_name, + config, + pending_payable_scanner, + receivable_scanner, + payable_scanner, + ); + subject.scan_schedulers.pending_payable.handle = Box::new( + NotifyLaterHandleMock::default() + .notify_later_params(¬ify_and_notify_later_params.pending_payables_notify_later), + ); let new_payable_expected_computed_interval = Duration::from_secs(3600); subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default() - .notify_later_params(&scan_for_new_payables_notify_later_params_arc), + .notify_later_params(¬ify_and_notify_later_params.new_payables_notify_later), ); subject.scan_schedulers.payable.retry_payable_notify = Box::new( - NotifyHandleMock::default().notify_params(&scan_for_retry_payables_notify_params_arc), + NotifyHandleMock::default() + .notify_params(¬ify_and_notify_later_params.retry_payables_notify), ); subject.scan_schedulers.payable.new_payable_notify = Box::new( - NotifyHandleMock::default().notify_params(&scan_for_new_payables_notify_params_arc), + NotifyHandleMock::default() + .notify_params(¬ify_and_notify_later_params.new_payables_notify), ); let receivable_notify_later_handle_mock = NotifyLaterHandleMock::default() - .notify_later_params(&scan_for_receivables_notify_later_params_arc) + .notify_later_params(¬ify_and_notify_later_params.receivables_notify_later) .stop_system_on_count_received(1); subject.scan_schedulers.receivable.handle = Box::new(receivable_notify_later_handle_mock); // Important that this is made short because the test relies on it with the system stop. @@ -2923,18 +2897,9 @@ mod tests { fn set_up_subject_for_some_pending_payable_found_startup_test( test_name: &str, - scan_for_pending_payables_notify_later_params_arc: &Arc< - Mutex>, - >, - scan_for_new_payables_notify_later_params_arc: &Arc< - Mutex>, - >, - scan_for_new_payables_notify_params_arc: &Arc>>, - scan_for_retry_payables_notify_params_arc: &Arc>>, - scan_for_receivables_notify_later_params_arc: &Arc< - Mutex>, - >, + notify_and_notify_later_params: &NotifyAndNotifyLaterParams, config: BootstrapperConfig, + payable_scanner: ScannerMock, pending_payable_scanner: ScannerMock< RequestTransactionReceipts, ReportTransactionReceipts, @@ -2945,29 +2910,16 @@ mod tests { ReceivedPayments, Option, >, - payable_scanner: ScannerMock, ) -> (Accountant, Duration, Duration) { - let mut subject = AccountantBuilder::default() - .logger(Logger::new(test_name)) - .bootstrapper_config(config) - .build(); - subject - .scanners - .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( - pending_payable_scanner, - ))); - subject - .scanners - .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Mock( - receivable_scanner, - ))); - subject - .scanners - .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( - payable_scanner, - ))); + let mut subject = make_subject_and_inject_scanners( + test_name, + config, + pending_payable_scanner, + receivable_scanner, + payable_scanner, + ); let pending_payable_notify_later_handle_mock = NotifyLaterHandleMock::default() - .notify_later_params(&scan_for_pending_payables_notify_later_params_arc) + .notify_later_params(¬ify_and_notify_later_params.pending_payables_notify_later) // This should stop the system .stop_system_on_count_received(1); subject.scan_schedulers.pending_payable.handle = @@ -2976,18 +2928,19 @@ mod tests { subject.scan_schedulers.pending_payable.interval = pending_payable_scan_interval; subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default() - .notify_later_params(&scan_for_new_payables_notify_later_params_arc), + .notify_later_params(¬ify_and_notify_later_params.new_payables_notify_later), ); subject.scan_schedulers.payable.retry_payable_notify = Box::new( NotifyHandleMock::default() - .notify_params(&scan_for_retry_payables_notify_params_arc) + .notify_params(¬ify_and_notify_later_params.retry_payables_notify) .capture_msg_and_let_it_fly_on(), ); subject.scan_schedulers.payable.new_payable_notify = Box::new( - NotifyHandleMock::default().notify_params(&scan_for_new_payables_notify_params_arc), + NotifyHandleMock::default() + .notify_params(¬ify_and_notify_later_params.new_payables_notify), ); let receivable_notify_later_handle_mock = NotifyLaterHandleMock::default() - .notify_later_params(&scan_for_receivables_notify_later_params_arc); + .notify_later_params(¬ify_and_notify_later_params.receivables_notify_later); let receivable_scan_interval = Duration::from_secs(3600); subject.scan_schedulers.receivable.interval = receivable_scan_interval; subject.scan_schedulers.receivable.handle = Box::new(receivable_notify_later_handle_mock); @@ -2998,23 +2951,60 @@ mod tests { ) } + fn make_subject_and_inject_scanners( + test_name: &str, + config: BootstrapperConfig, + pending_payable_scanner: ScannerMock< + RequestTransactionReceipts, + ReportTransactionReceipts, + PendingPayableScanResult, + >, + receivable_scanner: ScannerMock< + RetrieveTransactions, + ReceivedPayments, + Option, + >, + payable_scanner: ScannerMock, + ) -> Accountant { + let mut subject = AccountantBuilder::default() + .logger(Logger::new(test_name)) + .bootstrapper_config(config) + .build(); + subject + .scanners + .replace_scanner(ScannerReplacement::PendingPayable(ReplacementType::Mock( + pending_payable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Receivable(ReplacementType::Mock( + receivable_scanner, + ))); + subject + .scanners + .replace_scanner(ScannerReplacement::Payable(ReplacementType::Mock( + payable_scanner, + ))); + subject + } + fn assert_pending_payable_scanner_for_no_pending_payable_found( test_name: &str, consuming_wallet: Wallet, - pending_payable_start_scan_params_arc: Arc< + pending_payable_start_scan_params_arc: &Arc< Mutex, Logger, String)>>, >, - scan_for_pending_payables_notify_later_params_arc: Arc< + scan_for_pending_payables_notify_later_params_arc: &Arc< Mutex>, >, - before_performing_act: SystemTime, - after_performing_act: SystemTime, + act_started_at: SystemTime, + act_finished_at: SystemTime, ) { let pp_logger = pending_payable_common( consuming_wallet, pending_payable_start_scan_params_arc, - before_performing_act, - after_performing_act, + act_started_at, + act_finished_at, ); let scan_for_pending_payables_notify_later_params = scan_for_pending_payables_notify_later_params_arc @@ -3035,29 +3025,24 @@ mod tests { fn assert_pending_payable_scanner_for_some_pending_payable_found( test_name: &str, consuming_wallet: Wallet, - pending_payable_start_scan_params_arc: Arc< - Mutex, Logger, String)>>, - >, - pending_payable_finish_scan_params_arc: Arc< - Mutex>, - >, - scan_for_pending_payables_notify_later_params_arc: Arc< + scan_params: &ScanParams, + scan_for_pending_payables_notify_later_params_arc: &Arc< Mutex>, >, pending_payable_expected_notify_later_interval: Duration, expected_report_tx_receipts_msg: ReportTransactionReceipts, - before_perforrming_act: SystemTime, - after_perforrming_act: SystemTime, + act_started_at: SystemTime, + act_finished_at: SystemTime, ) { let pp_start_scan_logger = pending_payable_common( consuming_wallet, - pending_payable_start_scan_params_arc, - before_perforrming_act, - after_perforrming_act, + &scan_params.pending_payable_start_scan, + act_started_at, + act_finished_at, ); assert_using_the_same_logger(&pp_start_scan_logger, test_name, Some("pp start scan")); let mut pending_payable_finish_scan_params = - pending_payable_finish_scan_params_arc.lock().unwrap(); + scan_params.pending_payable_finish_scan.lock().unwrap(); let (actual_report_tx_receipts_msg, pp_finish_scan_logger) = pending_payable_finish_scan_params.remove(0); assert_eq!( @@ -3085,11 +3070,11 @@ mod tests { fn pending_payable_common( consuming_wallet: Wallet, - pending_payable_start_scan_params_arc: Arc< + pending_payable_start_scan_params_arc: &Arc< Mutex, Logger, String)>>, >, - before_performing_act: SystemTime, - after_performing_act: SystemTime, + act_started_at: SystemTime, + act_finished_at: SystemTime, ) -> Logger { let mut pending_payable_params = pending_payable_start_scan_params_arc.lock().unwrap(); let ( @@ -3112,33 +3097,28 @@ mod tests { pending_payable_params ); assert!( - before_performing_act <= pp_scan_started_at - && pp_scan_started_at <= after_performing_act, + act_started_at <= pp_scan_started_at && pp_scan_started_at <= act_finished_at, "The scanner was supposed to run between {:?} and {:?} but it was {:?}", - before_performing_act, - after_performing_act, + act_started_at, + act_finished_at, pp_scan_started_at ); pp_logger } fn assert_payable_scanner_for_no_pending_payable_found( - scan_for_new_payables_notify_later_params_arc: Arc< - Mutex>, - >, - new_payable_expected_computed_interval: Duration, - scan_for_new_payables_notify_params_arc: Arc>>, - scan_for_retry_payables_notify_params_arc: Arc>>, + notify_and_notify_later_params: &NotifyAndNotifyLaterParams, compute_interval_params_arc: Arc>>, - before_performing_act: SystemTime, - after_performing_act: SystemTime, + new_payable_expected_computed_interval: Duration, + act_started_at: SystemTime, + act_finished_at: SystemTime, ) { // Note that there is no functionality from the payable scanner actually running. // We only witness it to be scheduled. - let scan_for_new_payables_notify_later_params = - scan_for_new_payables_notify_later_params_arc - .lock() - .unwrap(); + let scan_for_new_payables_notify_later_params = notify_and_notify_later_params + .new_payables_notify_later + .lock() + .unwrap(); assert_eq!( *scan_for_new_payables_notify_later_params, vec![( @@ -3152,38 +3132,42 @@ mod tests { let (p_scheduling_now, last_new_payable_scan_timestamp, _) = compute_interval_params.remove(0); assert_eq!(last_new_payable_scan_timestamp, UNIX_EPOCH); - let scan_for_new_payables_notify_params = - scan_for_new_payables_notify_params_arc.lock().unwrap(); + let scan_for_new_payables_notify_params = notify_and_notify_later_params + .new_payables_notify + .lock() + .unwrap(); assert!( scan_for_new_payables_notify_params.is_empty(), "We did not expect any immediate scheduling of new payables, but it happened {:?}", scan_for_new_payables_notify_params ); - let scan_for_retry_payables_notify_params = - scan_for_retry_payables_notify_params_arc.lock().unwrap(); + let scan_for_retry_payables_notify_params = notify_and_notify_later_params + .retry_payables_notify + .lock() + .unwrap(); assert!( scan_for_retry_payables_notify_params.is_empty(), "We did not expect any scheduling of retry payables, but it happened {:?}", scan_for_retry_payables_notify_params ); - assert!(before_performing_act <= p_scheduling_now && p_scheduling_now <= after_performing_act, "The payable scan scheduling was supposed to take place between {:?} and {:?} but it was {:?}", before_performing_act, after_performing_act, p_scheduling_now); + assert!( + act_started_at <= p_scheduling_now && p_scheduling_now <= act_finished_at, + "The payable scan scheduling was supposed to take place between {:?} and {:?} \ + but it was {:?}", + act_started_at, + act_finished_at, + p_scheduling_now + ); } fn assert_payable_scanner_for_some_pending_payable_found( test_name: &str, consuming_wallet: Wallet, + scan_params: &ScanParams, + notify_and_notify_later_params: &NotifyAndNotifyLaterParams, expected_sent_payables: SentPayables, - payable_finish_scan_params_arc: Arc>>, - payable_start_scan_params_arc: Arc< - Mutex, Logger, String)>>, - >, - scan_for_new_payables_notify_later_params_arc: Arc< - Mutex>, - >, - scan_for_new_payables_notify_params_arc: Arc>>, - scan_for_retry_payables_notify_params_arc: Arc>>, ) { - let mut payable_start_scan_params = payable_start_scan_params_arc.lock().unwrap(); + let mut payable_start_scan_params = scan_params.payable_start_scan.lock().unwrap(); let (p_wallet, _, p_response_skeleton_opt, p_start_scan_logger, p_trigger_msg_type_str) = payable_start_scan_params.remove(0); assert_eq!(p_wallet, consuming_wallet); @@ -3200,7 +3184,7 @@ mod tests { payable_start_scan_params ); assert_using_the_same_logger(&p_start_scan_logger, test_name, Some("retry payable start")); - let mut payable_finish_scan_params = payable_finish_scan_params_arc.lock().unwrap(); + let mut payable_finish_scan_params = scan_params.payable_finish_scan.lock().unwrap(); let (actual_sent_payable, p_finish_scan_logger) = payable_finish_scan_params.remove(0); assert_eq!(actual_sent_payable, expected_sent_payables,); assert!( @@ -3213,24 +3197,28 @@ mod tests { test_name, Some("retry payable finish"), ); - let scan_for_new_payables_notify_later_params = - scan_for_new_payables_notify_later_params_arc - .lock() - .unwrap(); + let scan_for_new_payables_notify_later_params = notify_and_notify_later_params + .new_payables_notify_later + .lock() + .unwrap(); assert!( scan_for_new_payables_notify_later_params.is_empty(), "We did not expect any later scheduling of new payables, but it happened {:?}", scan_for_new_payables_notify_later_params ); - let scan_for_new_payables_notify_params = - scan_for_new_payables_notify_params_arc.lock().unwrap(); + let scan_for_new_payables_notify_params = notify_and_notify_later_params + .new_payables_notify + .lock() + .unwrap(); assert!( scan_for_new_payables_notify_params.is_empty(), "We did not expect any immediate scheduling of new payables, but it happened {:?}", scan_for_new_payables_notify_params ); - let scan_for_retry_payables_notify_params = - scan_for_retry_payables_notify_params_arc.lock().unwrap(); + let scan_for_retry_payables_notify_params = notify_and_notify_later_params + .retry_payables_notify + .lock() + .unwrap(); assert_eq!( *scan_for_retry_payables_notify_params, vec![ScanForRetryPayables { @@ -3242,13 +3230,13 @@ mod tests { fn assert_receivable_scanner( test_name: &str, earning_wallet: Wallet, - receivable_start_scan_params_arc: Arc< + receivable_start_scan_params_arc: &Arc< Mutex, Logger, String)>>, >, - receivable_scan_interval: Duration, - scan_for_receivables_notify_later_params_arc: Arc< + scan_for_receivables_notify_later_params_arc: &Arc< Mutex>, >, + receivable_scan_interval: Duration, ) { let mut receivable_start_scan_params = receivable_start_scan_params_arc.lock().unwrap(); let (r_wallet, _r_started_at, r_response_skeleton_opt, r_logger, r_trigger_msg_name_str) = From 4f07a225f1333ef3c2b2a4b0cf1cfdfeec7c1bdc Mon Sep 17 00:00:00 2001 From: Bert Date: Fri, 13 Jun 2025 16:23:48 +0200 Subject: [PATCH 48/49] GH-602: review two ends --- node/src/accountant/mod.rs | 10 ++-- node/src/test_utils/recorder.rs | 96 ++++++++++++++++++++++++++------- 2 files changed, 83 insertions(+), 23 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index bb6fc3f81..25a5a6a43 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -2860,11 +2860,13 @@ mod tests { receivable_scanner, payable_scanner, ); + let new_payable_expected_computed_interval = Duration::from_secs(3600); + // Important that this is made short because the test relies on it with the system stop. + let receivable_scan_interval = Duration::from_millis(50); subject.scan_schedulers.pending_payable.handle = Box::new( NotifyLaterHandleMock::default() .notify_later_params(¬ify_and_notify_later_params.pending_payables_notify_later), ); - let new_payable_expected_computed_interval = Duration::from_secs(3600); subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default() .notify_later_params(¬ify_and_notify_later_params.new_payables_notify_later), @@ -2881,8 +2883,6 @@ mod tests { .notify_later_params(¬ify_and_notify_later_params.receivables_notify_later) .stop_system_on_count_received(1); subject.scan_schedulers.receivable.handle = Box::new(receivable_notify_later_handle_mock); - // Important that this is made short because the test relies on it with the system stop. - let receivable_scan_interval = Duration::from_millis(50); subject.scan_schedulers.receivable.interval = receivable_scan_interval; let dyn_interval_computer = NewPayableScanDynIntervalComputerMock::default() .compute_interval_params(&compute_interval_params_arc) @@ -2918,13 +2918,14 @@ mod tests { receivable_scanner, payable_scanner, ); + let pending_payable_scan_interval = Duration::from_secs(3600); + let receivable_scan_interval = Duration::from_secs(3600); let pending_payable_notify_later_handle_mock = NotifyLaterHandleMock::default() .notify_later_params(¬ify_and_notify_later_params.pending_payables_notify_later) // This should stop the system .stop_system_on_count_received(1); subject.scan_schedulers.pending_payable.handle = Box::new(pending_payable_notify_later_handle_mock); - let pending_payable_scan_interval = Duration::from_secs(3600); subject.scan_schedulers.pending_payable.interval = pending_payable_scan_interval; subject.scan_schedulers.payable.new_payable_notify_later = Box::new( NotifyLaterHandleMock::default() @@ -2941,7 +2942,6 @@ mod tests { ); let receivable_notify_later_handle_mock = NotifyLaterHandleMock::default() .notify_later_params(¬ify_and_notify_later_params.receivables_notify_later); - let receivable_scan_interval = Duration::from_secs(3600); subject.scan_schedulers.receivable.interval = receivable_scan_interval; subject.scan_schedulers.receivable.handle = Box::new(receivable_notify_later_handle_mock); ( diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index 2636ae3b9..02265dd80 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -818,7 +818,7 @@ mod tests { } #[test] - fn diff_counter_msgs_with_diff_id_methods_can_be_used() { + fn counter_msgs_with_diff_id_methods_are_used_together_and_one_was_not_triggered() { let (respondent, _, respondent_recording_arc) = make_recorder(); let respondent = respondent.system_stop_conditions(match_lazily_every_type_id!( ScanForReceivables, @@ -826,12 +826,15 @@ mod tests { )); let respondent_addr = respondent.start(); // Case 1 + // This msg will trigger as the recorder will detect the arrival of StartMessage (no more + // requirement). let (trigger_message_1, cm_setup_1) = { let trigger_msg = StartMessage {}; let counter_msg = ScanForReceivables { response_skeleton_opt: None, }; - // Also testing this convenient macro if it works fine + // Taking an opportunity to test a setup via the macro for the simplest identification, + // by the TypeId. ( trigger_msg, setup_for_counter_msg_triggered_via_type_id!( @@ -842,6 +845,8 @@ mod tests { ) }; // Case two + // This msg will not trigger as it is declared with a wrong TypeId of the supposed trigger + // msg. The supplied ID does not even belong to an Actor msg type. let cm_setup_2 = { let counter_msg_strayed = StartMessage {}; let screwed_id = TypeId::of::(); @@ -856,6 +861,9 @@ mod tests { ) }; // Case three + // This msg will not trigger as it is declared to have to be matched entirely (The message + // type, plus the data of the message). The expected msg and the actual sent msg bear + // different IP addresses. let (trigger_msg_3_unmatching, cm_setup_3) = { let trigger_msg = NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(7, 7, 7, 7)), @@ -887,6 +895,7 @@ mod tests { ) }; // Case four + // This msg will trigger as the performed msg is an exact match of the expected msg. let (trigger_msg_4_matching, cm_setup_4, counter_msg_4) = { let trigger_msg = NewPublicIp { new_ip: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), @@ -920,7 +929,9 @@ mod tests { let (subject, _, subject_recording_arc) = make_recorder(); let subject_addr = subject.start(); // Supplying messages deliberately in a tangled manner to express that the mechanism is - // robust enough to compensate for it + // robust enough to compensate for it. + // This works because we don't supply overlapping setups, such as that could apply to + // a single arriving msg. That could subject_addr .try_send(SetUpCounterMsgs { setups: vec![cm_setup_3, cm_setup_1, cm_setup_2, cm_setup_4], @@ -936,7 +947,7 @@ mod tests { .unwrap(); system.run(); - // Actual counter-messages that flew over in this test + // Actual counter-messages that flew in this test let respondent_recording = respondent_recording_arc.lock().unwrap(); let _first_counter_msg_recorded = respondent_recording.get_record::(0); let second_counter_msg_recorded = respondent_recording.get_record::(1); @@ -953,8 +964,31 @@ mod tests { } #[test] - fn counter_msgs_of_same_type_are_checked_sequentially_and_triggered_by_first_matching_id_method( - ) { + fn counter_msgs_evaluate_lazily_so_the_msgs_with_the_same_triggers_are_eliminated_sequentially() + { + // This test demonstrates the need for caution in setups where multiple messages are sent + // at different times and should be responded to by different counter-messages. However, + // the trigger methods of these setups also apply to each other. Which setup gets + // triggered depends purely on the order used to supply them to the recorder + // in SetUpCounterMsgs. + + // Notice that three of the messages share the same data type, with one additional message + // serving a special purpose in assertions. Two of the three use only TypeId for + // identification. This already requires greater caution since you probably need the three + // messages to be dispatched in a specific sequence. However, this wasn't considered + // properly and, as you can see in the test, the trigger messages aren't sent in the same + // order as the counter-message setups were supplied. + + // This results in an inevitable mismatch. The first counter-message that was sent should + // have belonged to the second trigger message, but was triggered by the third trigger + // message (which actually introduces the test). Similarly, the second trigger message + // activates a message rightfully meant for the first trigger message. To complete + // the picture, even the first trigger message is matched with the third counter-message. + + // This shows how important it is to avoid ambiguous setups. When operating with multiple + // calls of the same typed message as triggers, it is highly recommended not to use + // MsgIdentification::ByTypeId but to use more specific, unmistakable settings instead: + // MsgIdentification::ByMatch or MsgIdentification::ByPredicate. let (respondent, _, respondent_recording_arc) = make_recorder(); let respondent = respondent.system_stop_conditions(match_lazily_every_type_id!( ConfigChangeMsg, @@ -980,6 +1014,8 @@ mod tests { }; ( trigger_msg, + // Taking an opportunity to test a setup via the macro allowing more specific + // identification methods. setup_for_counter_msg_triggered_via_specific_msg_id_method!( CrashNotification, id_method, @@ -1029,51 +1065,75 @@ mod tests { ), ) }; + // Case four + let (trigger_msg_4, cm_setup_4) = { + let trigger_msg = StartMessage {}; + let counter_msg = ScanForReceivables { + response_skeleton_opt: None, + }; + ( + trigger_msg, + setup_for_counter_msg_triggered_via_type_id!( + StartMessage, + counter_msg, + &respondent_addr + ), + ) + }; let system = System::new("test"); let (subject, _, subject_recording_arc) = make_recorder(); let subject_addr = subject.start(); // Adding messages in standard order subject_addr .try_send(SetUpCounterMsgs { - setups: vec![cm_setup_1, cm_setup_2, cm_setup_3], + setups: vec![cm_setup_1, cm_setup_2, cm_setup_3, cm_setup_4], }) .unwrap(); - // Trickier scenarios picked on purpose + // Now the fun begins, the trigger messages are shuffled subject_addr.try_send(trigger_msg_3.clone()).unwrap(); + // The fourth message demonstrates that the previous trigger didn't activate two messages + // at once, even though this trigger actually matches two different setups. This shows + // that each trigger can only be matched with one setup at a time, consuming it. If you + // want to trigger multiple messages in response, you must configure that setup with + // multiple counter-messages (a one-to-many scenario). + subject_addr.try_send(trigger_msg_4.clone()).unwrap(); subject_addr.try_send(trigger_msg_2.clone()).unwrap(); subject_addr.try_send(trigger_msg_1.clone()).unwrap(); system.run(); - // Actual counter messages that flew over in this test + // Actual counter-messages that flew in this test let respondent_recording = respondent_recording_arc.lock().unwrap(); let first_counter_msg_recorded = respondent_recording.get_record::(0); assert_eq!( first_counter_msg_recorded.change, ConfigChange::UpdatePassword("betterPassword".to_string()) ); - let second_counter_msg_recorded = respondent_recording.get_record::(1); + let _ = respondent_recording.get_record::(1); + let third_counter_msg_recorded = respondent_recording.get_record::(2); assert_eq!( - second_counter_msg_recorded.change, + third_counter_msg_recorded.change, ConfigChange::UpdateMinHops(Hops::SixHops) ); - let third_counter_msg_recorded = respondent_recording.get_record::(2); + let fourth_counter_msg_recorded = respondent_recording.get_record::(3); assert_eq!( - third_counter_msg_recorded.change, + fourth_counter_msg_recorded.change, ConfigChange::UpdateWallets(WalletPair { consuming_wallet: make_wallet("abc"), earning_wallet: make_wallet("def") }) ); - assert_eq!(respondent_recording.len(), 3); + assert_eq!(respondent_recording.len(), 4); // Recorded trigger messages let subject_recording = subject_recording_arc.lock().unwrap(); let first_recorded_trigger_msg = subject_recording.get_record::(0); assert_eq!(first_recorded_trigger_msg, &trigger_msg_3); - let second_recorded_trigger_msg = subject_recording.get_record::(1); - assert_eq!(second_recorded_trigger_msg, &trigger_msg_2); + let second_recorded_trigger_msg = subject_recording.get_record::(1); + assert_eq!(second_recorded_trigger_msg, &trigger_msg_4); let third_recorded_trigger_msg = subject_recording.get_record::(2); - assert_eq!(third_recorded_trigger_msg, &trigger_msg_1); - assert_eq!(subject_recording.len(), 3) + assert_eq!(third_recorded_trigger_msg, &trigger_msg_2); + let fourth_recorded_trigger_msg = subject_recording.get_record::(3); + assert_eq!(fourth_recorded_trigger_msg, &trigger_msg_1); + assert_eq!(subject_recording.len(), 4) } } From 90ae477d6aefaa111ed60a5982681602a161c620 Mon Sep 17 00:00:00 2001 From: Bert Date: Sat, 14 Jun 2025 11:17:13 +0200 Subject: [PATCH 49/49] GH-602: improved comments --- node/src/accountant/mod.rs | 7 +++---- node/src/test_utils/recorder.rs | 2 +- node/src/test_utils/recorder_counter_msgs.rs | 13 ++++++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 25a5a6a43..79f7c52f5 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -3010,10 +3010,9 @@ mod tests { scan_for_pending_payables_notify_later_params_arc .lock() .unwrap(); - // We stop the test right before running the `NewPayableScanner` and so that part is - // missing. We cannot capture the first occasion of scheduling the `PendingPayableScanner` - // any sooner than straight after the `NewPayableScan` finishes, if only it produced at - // least one blockchain transaction. + // PendingPayableScanner can only start after NewPayableScanner finishes and makes at least + // one transaction. The test stops before running NewPayableScanner, missing both + // the second PendingPayableScanner run and its scheduling event. assert!( scan_for_pending_payables_notify_later_params.is_empty(), "We did not expect to see another schedule for pending payables, but it happened {:?}", diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index 02265dd80..6633ee948 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -931,7 +931,7 @@ mod tests { // Supplying messages deliberately in a tangled manner to express that the mechanism is // robust enough to compensate for it. // This works because we don't supply overlapping setups, such as that could apply to - // a single arriving msg. That could + // a single trigger msg. subject_addr .try_send(SetUpCounterMsgs { setups: vec![cm_setup_3, cm_setup_1, cm_setup_2, cm_setup_4], diff --git a/node/src/test_utils/recorder_counter_msgs.rs b/node/src/test_utils/recorder_counter_msgs.rs index f2cef1520..9aa856fc2 100644 --- a/node/src/test_utils/recorder_counter_msgs.rs +++ b/node/src/test_utils/recorder_counter_msgs.rs @@ -13,15 +13,22 @@ use std::collections::HashMap; // a system. They enable sending either a single message or multiple messages in response to // a specific trigger, which is just another Actor message arriving at the Recorder. // By trigger, we mean the moment when an incoming message is tested sequentially against collected -// identification methods and matches. Each counter-message must have its identification method -// attached when it is being prepared for storage in the Recorder. +// identification methods and matches. Each counter-message must have its ID method attached when +// it is being prepared for storage in the Recorder. This bundle is called a setup. Each setup has +// one ID method but can contain multiple counter-messages that are all sent when triggered. + // Counter-messages can be independently customized and targeted at different actors by // providing their addresses, supporting complex interaction patterns. This design facilitates // sophisticated testing scenarios by mimicking real communication flows between multiple Actors. // The actual preparation of the Recorder needs to be carried out somewhat specifically during the // late stage of configuring the test, when all participating Actors are already started and their // addresses are known. The setup for counter-messages must be registered with the appropriate -// Recorder using a specially designated Actor message called `SetUpCounterMsgs`. +// Recorder using a specially designated Actor message SetUpCounterMsgs. + +// If a trigger message matches multiple counter-message setups, the triggered setup depends +// on the order in which setups are provided. Consider using MsgIdentification::ByMatch +// or MsgIdentification::ByPredicate instead of MsgIdentification::ByTypeId to avoid confusion +// about setup ordering. pub trait CounterMsgGear: Send { fn try_send(&self);