//! Transaction processing glue code, mainly consisting of Object-safe traits //! //! [InstalledSchedulerPool] lends one of pooled [InstalledScheduler]s as wrapped in //! [BankWithScheduler], which can be used by `ReplayStage` and `BankingStage` for transaction //! execution. After use, the scheduler will be returned to the pool. //! //! [InstalledScheduler] can be fed with [SanitizedTransaction]s. Then, it schedules those //! executions and commits those results into the associated _bank_. //! //! It's generally assumed that each [InstalledScheduler] is backed by multiple threads for //! parallel transaction processing and there are multiple independent schedulers inside a single //! instance of [InstalledSchedulerPool]. //! //! Dynamic dispatch was inevitable due to the desire to piggyback on //! [BankForks](crate::bank_forks::BankForks)'s pruning for scheduler lifecycle management as the //! common place both for `ReplayStage` and `BankingStage` and the resultant need of invoking //! actual implementations provided by the dependent crate (`solana-unified-scheduler-pool`, which //! in turn depends on `solana-ledger`, which in turn depends on `solana-runtime`), avoiding a //! cyclic dependency. //! //! See [InstalledScheduler] for visualized interaction. use { crate::bank::Bank, log::*, solana_program_runtime::timings::ExecuteTimings, solana_sdk::{ hash::Hash, slot_history::Slot, transaction::{Result, SanitizedTransaction}, }, std::{ fmt::Debug, ops::Deref, sync::{Arc, RwLock}, }, }; #[cfg(feature = "dev-context-only-utils")] use {mockall::automock, qualifier_attr::qualifiers}; pub trait InstalledSchedulerPool: Send + Sync + Debug { fn take_scheduler(&self, context: SchedulingContext) -> DefaultInstalledSchedulerBox; } #[cfg_attr(doc, aquamarine::aquamarine)] /// Schedules, executes, and commits transactions under encapsulated implementation /// /// The following chart illustrates the ownership/reference interaction between inter-dependent /// objects across crates: /// /// ```mermaid /// graph TD /// Bank["Arc#lt;Bank#gt;"] /// /// subgraph solana-runtime /// BankForks; /// BankWithScheduler; /// Bank; /// LoadExecuteAndCommitTransactions(["load_execute_and_commit_transactions()"]); /// SchedulingContext; /// InstalledSchedulerPool{{InstalledSchedulerPool}}; /// InstalledScheduler{{InstalledScheduler}}; /// end /// /// subgraph solana-unified-scheduler-pool /// SchedulerPool; /// PooledScheduler; /// ScheduleExecution(["schedule_execution()"]); /// end /// /// subgraph solana-ledger /// ExecuteBatch(["execute_batch()"]); /// end /// /// ScheduleExecution -. calls .-> ExecuteBatch; /// BankWithScheduler -. dyn-calls .-> ScheduleExecution; /// ExecuteBatch -. calls .-> LoadExecuteAndCommitTransactions; /// linkStyle 0,1,2 stroke:gray,color:gray; /// /// BankForks -- owns --> BankWithScheduler; /// BankForks -- owns --> InstalledSchedulerPool; /// BankWithScheduler -- refs --> Bank; /// BankWithScheduler -- owns --> InstalledScheduler; /// SchedulingContext -- refs --> Bank; /// InstalledScheduler -- owns --> SchedulingContext; /// /// SchedulerPool -- owns --> PooledScheduler; /// SchedulerPool -. impls .-> InstalledSchedulerPool; /// PooledScheduler -. impls .-> InstalledScheduler; /// PooledScheduler -- refs --> SchedulerPool; /// ``` #[cfg_attr(feature = "dev-context-only-utils", automock)] // suppress false clippy complaints arising from mockall-derive: // warning: `#[must_use]` has no effect when applied to a struct field // warning: the following explicit lifetimes could be elided: 'a #[cfg_attr( feature = "dev-context-only-utils", allow(unused_attributes, clippy::needless_lifetimes) )] pub trait InstalledScheduler: Send + Sync + Debug + 'static { fn id(&self) -> SchedulerId; fn context(&self) -> &SchedulingContext; // Calling this is illegal as soon as wait_for_termination is called. fn schedule_execution<'a>( &'a self, transaction_with_index: &'a (&'a SanitizedTransaction, usize), ); /// Wait for a scheduler to terminate after it is notified with the given reason. /// /// Firstly, this function blocks the current thread while waiting for the scheduler to /// complete all of the executions for the scheduled transactions. This means the scheduler has /// prepared the finalized `ResultWithTimings` at least internally at the time of existing from /// this function. If no trsanction is scheduled, the result and timing will be `Ok(())` and /// `ExecuteTimings::default()` respectively. This is done in the same way regardless of /// `WaitReason`. /// /// After that, the scheduler may behave differently depending on the reason, regarding the /// final bookkeeping. Specifically, this function guaranteed to return /// `Some(finalized_result_with_timings)` unless the reason is `PausedForRecentBlockhash`. In /// the case of `PausedForRecentBlockhash`, the scheduler is responsible to retain the /// finalized `ResultWithTimings` until it's `wait_for_termination()`-ed with one of the other /// two reasons later. #[must_use] fn wait_for_termination(&mut self, reason: &WaitReason) -> Option; fn return_to_pool(self: Box); } pub type DefaultInstalledSchedulerBox = Box; pub type InstalledSchedulerPoolArc = Arc; pub type SchedulerId = u64; /// A small context to propagate a bank and its scheduling mode to the scheduler subsystem. /// /// Note that this isn't called `SchedulerContext` because the contexts aren't associated with /// schedulers one by one. A scheduler will use many SchedulingContexts during its lifetime. /// "Scheduling" part of the context name refers to an abstract slice of time to schedule and /// execute all transactions for a given bank for block verification or production. A context is /// expected to be used by a particular scheduler only for that duration of the time and to be /// disposed by the scheduler. Then, the scheduler may work on different banks with new /// `SchedulingContext`s. #[derive(Clone, Debug)] pub struct SchedulingContext { // mode: SchedulingMode, // this will be added later. bank: Arc, } impl SchedulingContext { pub fn new(bank: Arc) -> Self { Self { bank } } pub fn bank(&self) -> &Arc { &self.bank } pub fn slot(&self) -> Slot { self.bank().slot() } } pub type ResultWithTimings = (Result<()>, ExecuteTimings); /// A hint from the bank about the reason the caller is waiting on its scheduler termination. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum WaitReason { // The bank wants its scheduler to terminate after the completion of transaction execution, in // order to freeze itself immediately thereafter. This is by far the most normal wait reason. // // Note that `wait_for_termination(TerminatedToFreeze)` must explicitly be done prior // to Bank::freeze(). This can't be done inside Bank::freeze() implicitly to remain it // infallible. TerminatedToFreeze, // The bank wants its scheduler to terminate just like `TerminatedToFreeze` and indicate that // Drop::drop() is the caller. DroppedFromBankForks, // The bank wants its scheduler to pause the scheduler after the completion without being // returned to the pool to collect scheduler's internally-held `ResultWithTimings` later. PausedForRecentBlockhash, } impl WaitReason { pub fn is_paused(&self) -> bool { // Exhaustive `match` is preferred here than `matches!()` to trigger an explicit // decision to be made, should we add new variants like `PausedForFooBar`... match self { WaitReason::PausedForRecentBlockhash => true, WaitReason::TerminatedToFreeze | WaitReason::DroppedFromBankForks => false, } } } /// Very thin wrapper around Arc /// /// It brings type-safety against accidental mixing of bank and scheduler with different slots, /// which is a pretty dangerous condition. Also, it guarantees to call wait_for_termination() via /// ::drop() inside BankForks::set_root()'s pruning, perfectly matching to Arc's lifetime by /// piggybacking on the pruning. /// /// Semantically, a scheduler is tightly coupled with a particular bank. But scheduler wasn't put /// into Bank fields to avoid circular-references (a scheduler needs to refer to its accompanied /// Arc). BankWithScheduler behaves almost like Arc. It only adds a few of transaction /// scheduling and scheduler management functions. For this reason, `bank` variable names should be /// used for `BankWithScheduler` across codebase. /// /// BankWithScheduler even implements Deref for convenience. And Clone is omitted to implement to /// avoid ambiguity as to which to clone: BankWithScheduler or Arc. Use /// clone_without_scheduler() for Arc. Otherwise, use clone_with_scheduler() (this should be /// unusual outside scheduler code-path) #[derive(Debug)] pub struct BankWithScheduler { inner: Arc, } #[derive(Debug)] pub struct BankWithSchedulerInner { bank: Arc, scheduler: InstalledSchedulerRwLock, } pub type InstalledSchedulerRwLock = RwLock>; impl BankWithScheduler { #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] pub(crate) fn new(bank: Arc, scheduler: Option) -> Self { if let Some(bank_in_context) = scheduler .as_ref() .map(|scheduler| scheduler.context().bank()) { assert!(Arc::ptr_eq(&bank, bank_in_context)); } Self { inner: Arc::new(BankWithSchedulerInner { bank, scheduler: RwLock::new(scheduler), }), } } pub fn new_without_scheduler(bank: Arc) -> Self { Self::new(bank, None) } pub fn clone_with_scheduler(&self) -> BankWithScheduler { BankWithScheduler { inner: self.inner.clone(), } } pub fn clone_without_scheduler(&self) -> Arc { self.inner.bank.clone() } pub fn register_tick(&self, hash: &Hash) { self.inner.bank.register_tick(hash, &self.inner.scheduler); } #[cfg(feature = "dev-context-only-utils")] pub fn fill_bank_with_ticks_for_tests(&self) { self.do_fill_bank_with_ticks_for_tests(&self.inner.scheduler); } pub fn has_installed_scheduler(&self) -> bool { self.inner.scheduler.read().unwrap().is_some() } // 'a is needed; anonymous_lifetime_in_impl_trait isn't stabilized yet... pub fn schedule_transaction_executions<'a>( &self, transactions_with_indexes: impl ExactSizeIterator, ) { trace!( "schedule_transaction_executions(): {} txs", transactions_with_indexes.len() ); let scheduler_guard = self.inner.scheduler.read().unwrap(); let scheduler = scheduler_guard.as_ref().unwrap(); for (sanitized_transaction, &index) in transactions_with_indexes { scheduler.schedule_execution(&(sanitized_transaction, index)); } } // take needless &mut only to communicate its semantic mutability to humans... #[cfg(feature = "dev-context-only-utils")] pub fn drop_scheduler(&mut self) { self.inner.drop_scheduler(); } pub(crate) fn wait_for_paused_scheduler(bank: &Bank, scheduler: &InstalledSchedulerRwLock) { let maybe_result_with_timings = BankWithSchedulerInner::wait_for_scheduler_termination( bank, scheduler, WaitReason::PausedForRecentBlockhash, ); assert!( maybe_result_with_timings.is_none(), "Premature result was returned from scheduler after paused" ); } #[must_use] pub fn wait_for_completed_scheduler(&self) -> Option { BankWithSchedulerInner::wait_for_scheduler_termination( &self.inner.bank, &self.inner.scheduler, WaitReason::TerminatedToFreeze, ) } pub const fn no_scheduler_available() -> InstalledSchedulerRwLock { RwLock::new(None) } } impl BankWithSchedulerInner { #[must_use] fn wait_for_completed_scheduler_from_drop(&self) -> Option { Self::wait_for_scheduler_termination( &self.bank, &self.scheduler, WaitReason::DroppedFromBankForks, ) } #[must_use] fn wait_for_scheduler_termination( bank: &Bank, scheduler: &InstalledSchedulerRwLock, reason: WaitReason, ) -> Option { debug!( "wait_for_scheduler_termination(slot: {}, reason: {:?}): started...", bank.slot(), reason, ); let mut scheduler = scheduler.write().unwrap(); let result_with_timings = if scheduler.is_some() { let result_with_timings = scheduler .as_mut() .and_then(|scheduler| scheduler.wait_for_termination(&reason)); if !reason.is_paused() { let scheduler = scheduler.take().expect("scheduler after waiting"); scheduler.return_to_pool(); } result_with_timings } else { None }; debug!( "wait_for_scheduler_termination(slot: {}, reason: {:?}): finished with: {:?}...", bank.slot(), reason, result_with_timings.as_ref().map(|(result, _)| result), ); result_with_timings } fn drop_scheduler(&self) { if std::thread::panicking() { error!( "BankWithSchedulerInner::drop_scheduler(): slot: {} skipping due to already panicking...", self.bank.slot(), ); return; } // There's no guarantee ResultWithTimings is available or not at all when being dropped. if let Some(Err(err)) = self .wait_for_completed_scheduler_from_drop() .map(|(result, _timings)| result) { warn!( "BankWithSchedulerInner::drop_scheduler(): slot: {} discarding error from scheduler: {:?}", self.bank.slot(), err, ); } } } impl Drop for BankWithSchedulerInner { fn drop(&mut self) { self.drop_scheduler(); } } impl Deref for BankWithScheduler { type Target = Arc; fn deref(&self) -> &Self::Target { &self.inner.bank } } #[cfg(test)] mod tests { use { super::*, crate::{ bank::test_utils::goto_end_of_slot_with_scheduler, genesis_utils::{create_genesis_config, GenesisConfigInfo}, }, assert_matches::assert_matches, mockall::Sequence, solana_sdk::system_transaction, }; fn setup_mocked_scheduler_with_extra( bank: Arc, wait_reasons: impl Iterator, f: Option, ) -> DefaultInstalledSchedulerBox { let mut mock = MockInstalledScheduler::new(); let mut seq = Sequence::new(); mock.expect_context() .times(1) .in_sequence(&mut seq) .return_const(SchedulingContext::new(bank)); for wait_reason in wait_reasons { mock.expect_wait_for_termination() .with(mockall::predicate::eq(wait_reason)) .times(1) .in_sequence(&mut seq) .returning(move |_| { if wait_reason.is_paused() { None } else { Some((Ok(()), ExecuteTimings::default())) } }); } mock.expect_return_to_pool() .times(1) .in_sequence(&mut seq) .returning(|| ()); if let Some(f) = f { f(&mut mock); } Box::new(mock) } fn setup_mocked_scheduler( bank: Arc, wait_reasons: impl Iterator, ) -> DefaultInstalledSchedulerBox { setup_mocked_scheduler_with_extra( bank, wait_reasons, None:: ()>, ) } #[test] fn test_scheduler_normal_termination() { solana_logger::setup(); let bank = Arc::new(Bank::default_for_tests()); let bank = BankWithScheduler::new( bank.clone(), Some(setup_mocked_scheduler( bank, [WaitReason::TerminatedToFreeze].into_iter(), )), ); assert!(bank.has_installed_scheduler()); assert_matches!(bank.wait_for_completed_scheduler(), Some(_)); // Repeating to call wait_for_completed_scheduler() is okay with no ResultWithTimings being // returned. assert!(!bank.has_installed_scheduler()); assert_matches!(bank.wait_for_completed_scheduler(), None); } #[test] fn test_no_scheduler_termination() { solana_logger::setup(); let bank = Arc::new(Bank::default_for_tests()); let bank = BankWithScheduler::new_without_scheduler(bank); // Calling wait_for_completed_scheduler() is noop, when no scheduler is installed. assert!(!bank.has_installed_scheduler()); assert_matches!(bank.wait_for_completed_scheduler(), None); } #[test] fn test_scheduler_termination_from_drop() { solana_logger::setup(); let bank = Arc::new(Bank::default_for_tests()); let bank = BankWithScheduler::new( bank.clone(), Some(setup_mocked_scheduler( bank, [WaitReason::DroppedFromBankForks].into_iter(), )), ); drop(bank); } #[test] fn test_scheduler_pause() { solana_logger::setup(); let bank = Arc::new(crate::bank::tests::create_simple_test_bank(42)); let bank = BankWithScheduler::new( bank.clone(), Some(setup_mocked_scheduler( bank, [ WaitReason::PausedForRecentBlockhash, WaitReason::TerminatedToFreeze, ] .into_iter(), )), ); goto_end_of_slot_with_scheduler(&bank); assert_matches!(bank.wait_for_completed_scheduler(), Some(_)); } #[test] fn test_schedule_executions() { solana_logger::setup(); let GenesisConfigInfo { genesis_config, mint_keypair, .. } = create_genesis_config(10_000); let tx0 = SanitizedTransaction::from_transaction_for_tests(system_transaction::transfer( &mint_keypair, &solana_sdk::pubkey::new_rand(), 2, genesis_config.hash(), )); let bank = Arc::new(Bank::new_for_tests(&genesis_config)); let mocked_scheduler = setup_mocked_scheduler_with_extra( bank.clone(), [WaitReason::DroppedFromBankForks].into_iter(), Some(|mocked: &mut MockInstalledScheduler| { mocked .expect_schedule_execution() .times(1) .returning(|(_, _)| ()); }), ); let bank = BankWithScheduler::new(bank, Some(mocked_scheduler)); bank.schedule_transaction_executions([(&tx0, &0)].into_iter()); } }