Skip to main content

solana_runtime/
installed_scheduler_pool.rs

1//! Transaction processing glue code, mainly consisting of Object-safe traits
2//!
3//! [InstalledSchedulerPool] lends one of pooled [InstalledScheduler]s as wrapped in
4//! [BankWithScheduler], which can be used by `ReplayStage` and `BankingStage` for transaction
5//! execution. After use, the scheduler will be returned to the pool.
6//!
7//! [InstalledScheduler] can be fed with [SanitizedTransaction]s. Then, it schedules those
8//! executions and commits those results into the associated _bank_.
9//!
10//! It's generally assumed that each [InstalledScheduler] is backed by multiple threads for
11//! parallel transaction processing and there are multiple independent schedulers inside a single
12//! instance of [InstalledSchedulerPool].
13//!
14//! Dynamic dispatch was inevitable due to the desire to piggyback on
15//! [BankForks](crate::bank_forks::BankForks)'s pruning for scheduler lifecycle management as the
16//! common place both for `ReplayStage` and `BankingStage` and the resultant need of invoking
17//! actual implementations provided by the dependent crate (`solana-unified-scheduler-pool`, which
18//! in turn depends on `solana-ledger`, which in turn depends on `solana-runtime`), avoiding a
19//! cyclic dependency.
20//!
21//! See [InstalledScheduler] for visualized interaction.
22
23use {
24    crate::bank::Bank,
25    log::*,
26    solana_clock::Slot,
27    solana_hash::Hash,
28    solana_runtime_transaction::runtime_transaction::RuntimeTransaction,
29    solana_svm_timings::ExecuteTimings,
30    solana_transaction::sanitized::SanitizedTransaction,
31    solana_transaction_error::{TransactionError, TransactionResult as Result},
32    solana_unified_scheduler_logic::OrderedTaskId,
33    std::{
34        fmt::{self, Debug},
35        mem,
36        ops::Deref,
37        sync::{Arc, RwLock},
38        thread,
39    },
40};
41#[cfg(feature = "dev-context-only-utils")]
42use {mockall::automock, qualifier_attr::qualifiers};
43
44pub fn initialized_result_with_timings() -> ResultWithTimings {
45    (Ok(()), ExecuteTimings::default())
46}
47
48pub trait InstalledSchedulerPool: Send + Sync + Debug {
49    /// A very thin wrapper of [`Self::take_resumed_scheduler`] to take a scheduler from this pool
50    /// for a brand-new bank.
51    fn take_scheduler(&self, context: SchedulingContext) -> Option<InstalledSchedulerBox> {
52        self.take_resumed_scheduler(context, initialized_result_with_timings())
53    }
54
55    fn take_resumed_scheduler(
56        &self,
57        context: SchedulingContext,
58        result_with_timings: ResultWithTimings,
59    ) -> Option<InstalledSchedulerBox>;
60
61    /// Registers an opaque timeout listener.
62    ///
63    /// This method and the passed `struct` called [`TimeoutListener`] are very opaque by purpose.
64    /// Specifically, it doesn't provide any way to tell which listener is semantically associated
65    /// to which particular scheduler. That's because proper _unregistration_ is omitted at the
66    /// timing of scheduler returning to reduce latency of the normal block-verification code-path,
67    /// relying on eventual stale listener clean-up by `solScCleaner`.
68    fn register_timeout_listener(&self, timeout_listener: TimeoutListener);
69
70    fn uninstalled_from_bank_forks(self: Arc<Self>);
71}
72
73#[derive(Debug)]
74pub struct SchedulerAborted;
75pub type ScheduleResult = std::result::Result<(), SchedulerAborted>;
76
77pub struct TimeoutListener {
78    callback: Box<dyn FnOnce(InstalledSchedulerPoolArc) + Sync + Send>,
79}
80
81impl TimeoutListener {
82    pub(crate) fn new(f: impl FnOnce(InstalledSchedulerPoolArc) + Sync + Send + 'static) -> Self {
83        Self {
84            callback: Box::new(f),
85        }
86    }
87
88    pub fn trigger(self, pool: InstalledSchedulerPoolArc) {
89        (self.callback)(pool);
90    }
91}
92
93impl Debug for TimeoutListener {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        write!(f, "TimeoutListener({self:p})")
96    }
97}
98
99#[cfg_attr(doc, aquamarine::aquamarine)]
100/// Schedules, executes, and commits transactions under encapsulated implementation
101///
102/// The following chart illustrates the ownership/reference interaction between inter-dependent
103/// objects across crates:
104///
105/// ```mermaid
106/// graph TD
107///     Bank["Arc#lt;Bank#gt;"]
108///
109///     subgraph solana-runtime[<span style="font-size: 70%">solana-runtime</span>]
110///         BankForks;
111///         BankWithScheduler;
112///         Bank;
113///         LoadExecuteAndCommitTransactions([<span style="font-size: 67%">load_execute_and_commit_transactions#lpar;#rpar;</span>]);
114///         SchedulingContext;
115///         InstalledSchedulerPool{{InstalledSchedulerPool}};
116///         InstalledScheduler{{InstalledScheduler}};
117///     end
118///
119///     subgraph solana-unified-scheduler-pool[<span style="font-size: 70%">solana-unified-scheduler-pool</span>]
120///         SchedulerPool;
121///         PooledScheduler;
122///         ScheduleExecution(["schedule_execution()"]);
123///     end
124///
125///     subgraph solana-ledger[<span style="font-size: 60%">solana-ledger</span>]
126///         ExecuteBatch(["execute_batch()"]);
127///     end
128///
129///     ScheduleExecution -. calls .-> ExecuteBatch;
130///     BankWithScheduler -. dyn-calls .-> ScheduleExecution;
131///     ExecuteBatch -. calls .-> LoadExecuteAndCommitTransactions;
132///     linkStyle 0,1,2 stroke:gray,color:gray;
133///
134///     BankForks -- owns --> BankWithScheduler;
135///     BankForks -- owns --> InstalledSchedulerPool;
136///     BankWithScheduler -- refs --> Bank;
137///     BankWithScheduler -- owns --> InstalledScheduler;
138///     SchedulingContext -- refs --> Bank;
139///     InstalledScheduler -- owns --> SchedulingContext;
140///
141///     SchedulerPool -- owns --> PooledScheduler;
142///     SchedulerPool -. impls .-> InstalledSchedulerPool;
143///     PooledScheduler -. impls .-> InstalledScheduler;
144///     PooledScheduler -- refs --> SchedulerPool;
145/// ```
146#[cfg_attr(feature = "dev-context-only-utils", automock)]
147// suppress false clippy complaints arising from mockall-derive:
148//   warning: `#[must_use]` has no effect when applied to a struct field
149#[cfg_attr(feature = "dev-context-only-utils", allow(unused_attributes))]
150pub trait InstalledScheduler: Send + Sync + Debug + 'static {
151    fn id(&self) -> SchedulerId;
152    fn context(&self) -> &SchedulingContext;
153
154    /// Schedule transaction for execution.
155    ///
156    /// This non-blocking function will return immediately without waiting for actual execution.
157    ///
158    /// Calling this is illegal as soon as `wait_for_termination()` is called. It would result in
159    /// fatal logic error.
160    ///
161    /// Note that the returned result indicates whether the scheduler has been aborted due to a
162    /// previously-scheduled bad transaction, which terminates further block verification. So,
163    /// almost always, the returned error isn't due to the merely scheduling of the current
164    /// transaction itself. At this point, calling this does nothing anymore while it's still safe
165    /// to do. As soon as notified, callers are expected to stop processing upcoming transactions
166    /// of the same `SchedulingContext` (i.e. same block). Internally, the aborted scheduler will
167    /// be disposed cleanly, not repooled, after `wait_for_termination()` is called like
168    /// not-aborted schedulers.
169    ///
170    /// Caller can acquire the error by calling a separate function called
171    /// `recover_error_after_abort()`, which requires `&mut self`, instead of `&self`. This
172    /// separation and the convoluted returned value semantics explained above are intentional to
173    /// optimize the fast code-path of normal transaction scheduling to be multi-threaded at the
174    /// cost of far slower error code-path while giving implementors increased flexibility by
175    /// having &mut.
176    fn schedule_execution(
177        &self,
178        transaction: RuntimeTransaction<SanitizedTransaction>,
179        task_id: OrderedTaskId,
180    ) -> ScheduleResult;
181
182    /// Return the error which caused the scheduler to abort.
183    ///
184    /// Note that this must not be called until it's observed that `schedule_execution()` has
185    /// returned `Err(SchedulerAborted)`. Violating this should `panic!()`.
186    ///
187    /// That said, calling this multiple times is completely acceptable after the error observation
188    /// from `schedule_execution()`. While it's not guaranteed, the same `.clone()`-ed errors of
189    /// the first bad transaction are usually returned across invocations.
190    fn recover_error_after_abort(&mut self) -> TransactionError;
191
192    /// Wait for a scheduler to terminate after processing.
193    ///
194    /// This function blocks the current thread while waiting for the scheduler to complete all of
195    /// the executions for the scheduled transactions and to return the finalized
196    /// `ResultWithTimings`. This function still blocks for short period of time even in the case
197    /// of aborted schedulers to gracefully shutdown the scheduler (like thread joining).
198    ///
199    /// Along with the result being returned, this function also makes the scheduler itself
200    /// uninstalled from the bank by transforming the consumed self.
201    ///
202    /// If no transaction is scheduled, the result and timing will be `Ok(())` and
203    /// `ExecuteTimings::default()` respectively.
204    fn wait_for_termination(
205        self: Box<Self>,
206        is_dropped: bool,
207    ) -> (ResultWithTimings, UninstalledSchedulerBox);
208
209    /// Pause a scheduler after processing to update bank's recent blockhash.
210    ///
211    /// This function blocks the current thread like wait_for_termination(). However, the scheduler
212    /// won't be consumed. This means the scheduler is responsible to retain the finalized
213    /// `ResultWithTimings` internally until it's `wait_for_termination()`-ed to collect the result
214    /// later.
215    fn pause_for_recent_blockhash(&mut self);
216
217    /// Unpause a block production scheduler, immediately after it's taken from the scheduler pool.
218    ///
219    /// This is rather a special-purposed method. Such a scheduler is initially paused due to a
220    /// race condition between the poh thread and handler threads. So, it needs to be unpaused in
221    /// order to start processing transactions by calling this.
222    ///
223    /// # Panics
224    ///
225    /// Panics if called on a block verification scheduler.
226    fn unpause_after_taken(&self);
227}
228
229#[cfg_attr(feature = "dev-context-only-utils", automock)]
230pub trait UninstalledScheduler: Send + Sync + Debug + 'static {
231    fn return_to_pool(self: Box<Self>);
232}
233
234pub type InstalledSchedulerBox = Box<dyn InstalledScheduler>;
235pub type UninstalledSchedulerBox = Box<dyn UninstalledScheduler>;
236
237pub type InstalledSchedulerPoolArc = Arc<dyn InstalledSchedulerPool>;
238
239pub type SchedulerId = u64;
240
241/// A small context to propagate a bank and its scheduling mode to the scheduler subsystem.
242///
243/// Note that this isn't called `SchedulerContext` because the contexts aren't associated with
244/// schedulers one by one. A scheduler will use many SchedulingContexts during its lifetime.
245/// "Scheduling" part of the context name refers to an abstract slice of time to schedule and
246/// execute all transactions for a given bank for block verification or production. A context is
247/// expected to be used by a particular scheduler only for that duration of the time and to be
248/// disposed by the scheduler. Then, the scheduler may work on different banks with new
249/// `SchedulingContext`s.
250///
251/// There's a special construction only used for scheduler preallocation, which has no bank. Panics
252/// will be triggered when tried to be used normally across code-base.
253#[derive(Clone, Debug)]
254pub struct SchedulingContext {
255    bank: Arc<Bank>,
256}
257
258impl SchedulingContext {
259    #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))]
260    pub(crate) fn new(bank: Arc<Bank>) -> Self {
261        Self { bank }
262    }
263
264    pub fn bank(&self) -> &Arc<Bank> {
265        &self.bank
266    }
267
268    pub fn slot(&self) -> Slot {
269        self.bank.slot()
270    }
271}
272
273pub type ResultWithTimings = (Result<()>, ExecuteTimings);
274
275/// A hint from the bank about the reason the caller is waiting on its scheduler.
276#[derive(Debug, PartialEq, Eq, Clone, Copy)]
277enum WaitReason {
278    // The bank wants its scheduler to terminate after the completion of transaction execution, in
279    // order to freeze itself immediately thereafter. This is by far the most normal wait reason.
280    //
281    // Note that `wait_for_termination(TerminatedToFreeze)` must explicitly be done prior
282    // to Bank::freeze(). This can't be done inside Bank::freeze() implicitly to remain it
283    // infallible.
284    TerminatedToFreeze,
285    // The bank wants its scheduler to terminate just like `TerminatedToFreeze` and indicate that
286    // Drop::drop() is the caller.
287    DroppedFromBankForks,
288    // The bank wants its scheduler to pause after the completion without being returned to the
289    // pool. This is to update bank's recent blockhash and to collect scheduler's internally-held
290    // `ResultWithTimings` later.
291    PausedForRecentBlockhash,
292}
293
294impl WaitReason {
295    pub fn is_paused(&self) -> bool {
296        // Exhaustive `match` is preferred here than `matches!()` to trigger an explicit
297        // decision to be made, should we add new variants like `PausedForFooBar`...
298        match self {
299            WaitReason::PausedForRecentBlockhash => true,
300            WaitReason::TerminatedToFreeze | WaitReason::DroppedFromBankForks => false,
301        }
302    }
303
304    pub fn is_dropped(&self) -> bool {
305        // Exhaustive `match` is preferred here than `matches!()` to trigger an explicit
306        // decision to be made, should we add new variants like `PausedForFooBar`...
307        match self {
308            WaitReason::DroppedFromBankForks => true,
309            WaitReason::TerminatedToFreeze | WaitReason::PausedForRecentBlockhash => false,
310        }
311    }
312}
313
314#[expect(clippy::large_enum_variant)]
315#[derive(Debug)]
316pub enum SchedulerStatus {
317    /// Unified scheduler is disabled or installed scheduler is consumed by
318    /// [`InstalledScheduler::wait_for_termination`]. Note that transition to [`Self::Unavailable`]
319    /// from {[`Self::Active`], [`Self::Stale`]} is one-way (i.e. one-time) unlike [`Self::Active`]
320    /// <=> [`Self::Stale`] below.  Also, this variant is transiently used as a placeholder
321    /// internally when transitioning scheduler statuses, which isn't observable unless panic is
322    /// happening.
323    Unavailable,
324    /// Scheduler is installed into a bank; could be running or just be waiting for additional
325    /// transactions. This will be transitioned to [`Self::Stale`] after certain time (i.e.
326    /// `solana_unified_scheduler_pool::DEFAULT_TIMEOUT_DURATION`) has passed if its bank hasn't
327    /// been frozen since installed.
328    Active(InstalledSchedulerBox),
329    /// Scheduler has yet to freeze its associated bank even after it's taken too long since
330    /// installed, resulting in returning the scheduler back to the pool. Later, this can
331    /// immediately (i.e. transparently) be transitioned to [`Self::Active`] as soon as there's new
332    /// transaction to be executed (= [`BankWithScheduler::schedule_transaction_executions`] is
333    /// called, which internally calls [`BankWithSchedulerInner::with_active_scheduler`] to make
334    /// the transition happen).
335    Stale(InstalledSchedulerPoolArc, ResultWithTimings),
336}
337
338impl SchedulerStatus {
339    fn new(scheduler: Option<InstalledSchedulerBox>) -> Self {
340        match scheduler {
341            Some(scheduler) => SchedulerStatus::Active(scheduler),
342            None => SchedulerStatus::Unavailable,
343        }
344    }
345
346    fn transition_from_stale_to_active(
347        &mut self,
348        f: impl FnOnce(InstalledSchedulerPoolArc, ResultWithTimings) -> InstalledSchedulerBox,
349    ) {
350        let Self::Stale(pool, result_with_timings) = mem::replace(self, Self::Unavailable) else {
351            panic!("transition to Active failed: {self:?}");
352        };
353        *self = Self::Active(f(pool, result_with_timings));
354    }
355
356    fn maybe_transition_from_active_to_stale(
357        &mut self,
358        f: impl FnOnce(InstalledSchedulerBox) -> (InstalledSchedulerPoolArc, ResultWithTimings),
359    ) {
360        if !matches!(self, Self::Active(_scheduler)) {
361            return;
362        }
363        let Self::Active(scheduler) = mem::replace(self, Self::Unavailable) else {
364            unreachable!("not active: {self:?}");
365        };
366        let (pool, result_with_timings) = f(scheduler);
367        *self = Self::Stale(pool, result_with_timings);
368    }
369
370    fn transition_from_active_to_unavailable(&mut self) -> InstalledSchedulerBox {
371        let Self::Active(scheduler) = mem::replace(self, Self::Unavailable) else {
372            panic!("transition to Unavailable failed: {self:?}");
373        };
374        scheduler
375    }
376
377    fn transition_from_stale_to_unavailable(&mut self) -> ResultWithTimings {
378        let Self::Stale(_pool, result_with_timings) = mem::replace(self, Self::Unavailable) else {
379            panic!("transition to Unavailable failed: {self:?}");
380        };
381        result_with_timings
382    }
383
384    fn active_scheduler(&self) -> &InstalledSchedulerBox {
385        let SchedulerStatus::Active(active_scheduler) = self else {
386            panic!("not active: {self:?}");
387        };
388        active_scheduler
389    }
390}
391
392/// Very thin wrapper around Arc<Bank>
393///
394/// It brings type-safety against accidental mixing of bank and scheduler with different slots,
395/// which is a pretty dangerous condition. Also, it guarantees to call wait_for_termination() via
396/// ::drop() by DropBankService, which receives Vec<BankWithScheduler> from BankForks::set_root()'s
397/// pruning, mostly matching to Arc<Bank>'s lifetime by piggybacking on the pruning.
398///
399/// Semantically, a scheduler is tightly coupled with a particular bank. But scheduler wasn't put
400/// into Bank fields to avoid circular-references (a scheduler needs to refer to its accompanied
401/// Arc<Bank>). BankWithScheduler behaves almost like Arc<Bank>. It only adds a few of transaction
402/// scheduling and scheduler management functions. For this reason, `bank` variable names should be
403/// used for `BankWithScheduler` across codebase.
404///
405/// BankWithScheduler even implements Deref for convenience. And Clone is omitted to implement to
406/// avoid ambiguity as to which to clone: BankWithScheduler or Arc<Bank>. Use
407/// clone_without_scheduler() for Arc<Bank>. Otherwise, use clone_with_scheduler() (this should be
408/// unusual outside scheduler code-path)
409#[derive(Debug)]
410pub struct BankWithScheduler {
411    inner: Arc<BankWithSchedulerInner>,
412}
413
414#[derive(Debug)]
415pub struct BankWithSchedulerInner {
416    bank: Arc<Bank>,
417    scheduler: InstalledSchedulerRwLock,
418}
419pub type InstalledSchedulerRwLock = RwLock<SchedulerStatus>;
420
421impl BankWithScheduler {
422    /// Creates a new `BankWithScheduler` from bank and its associated scheduler.
423    ///
424    /// # Panics
425    ///
426    /// Panics if `scheduler`'s scheduling context is unmatched to given bank or for scheduler
427    /// preallocation.
428    #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))]
429    pub(crate) fn new(bank: Arc<Bank>, scheduler: Option<InstalledSchedulerBox>) -> Self {
430        // Avoid the fatal situation in which bank is being associated with a scheduler associated
431        // to a different bank!
432        if let Some(bank_in_context) = scheduler
433            .as_ref()
434            .map(|scheduler| scheduler.context().bank())
435        {
436            assert!(Arc::ptr_eq(&bank, bank_in_context));
437        }
438
439        Self {
440            inner: Arc::new(BankWithSchedulerInner {
441                bank,
442                scheduler: RwLock::new(SchedulerStatus::new(scheduler)),
443            }),
444        }
445    }
446
447    pub fn new_without_scheduler(bank: Arc<Bank>) -> Self {
448        Self::new(bank, None)
449    }
450
451    pub fn clone_with_scheduler(&self) -> BankWithScheduler {
452        BankWithScheduler {
453            inner: self.inner.clone(),
454        }
455    }
456
457    pub fn clone_without_scheduler(&self) -> Arc<Bank> {
458        self.inner.bank.clone()
459    }
460
461    pub fn register_tick(&self, hash: &Hash) {
462        self.inner.bank.register_tick(hash, &self.inner.scheduler);
463    }
464
465    #[cfg(feature = "dev-context-only-utils")]
466    pub fn fill_bank_with_ticks_for_tests(&self) {
467        self.do_fill_bank_with_ticks_for_tests(&self.inner.scheduler);
468    }
469
470    pub fn has_installed_scheduler(&self) -> bool {
471        !matches!(
472            &*self.inner.scheduler.read().unwrap(),
473            SchedulerStatus::Unavailable
474        )
475    }
476
477    /// Schedule the transaction as long as the scheduler hasn't been aborted.
478    ///
479    /// If the scheduler has been aborted, this doesn't schedule the transaction, instead just
480    /// return the error of prior scheduled transaction.
481    ///
482    /// Calling this will panic if the installed scheduler is Unavailable (the bank is
483    /// wait_for_termination()-ed or the unified scheduler is disabled in the first place).
484    pub fn schedule_transaction_executions(
485        &self,
486        transaction_with_task_ids: impl ExactSizeIterator<
487            Item = (RuntimeTransaction<SanitizedTransaction>, OrderedTaskId),
488        >,
489    ) -> Result<()> {
490        trace!(
491            "schedule_transaction_executions(): {} txs",
492            transaction_with_task_ids.len()
493        );
494
495        let schedule_result: ScheduleResult = self.inner.with_active_scheduler(|scheduler| {
496            for (sanitized_transaction, task_id) in transaction_with_task_ids {
497                scheduler.schedule_execution(sanitized_transaction, task_id)?;
498            }
499            Ok(())
500        });
501
502        if schedule_result.is_err() {
503            // This write lock isn't atomic with the above the read lock. So, another thread
504            // could have called .recover_error_after_abort() while we're literally stuck at
505            // the gaps of these locks (i.e. this comment in source code wise) under extreme
506            // race conditions. Thus, .recover_error_after_abort() is made idempotetnt for that
507            // consideration in mind.
508            //
509            // Lastly, this non-atomic nature is intentional for optimizing the fast code-path
510            return Err(self.inner.retrieve_error_after_schedule_failure());
511        }
512
513        Ok(())
514    }
515
516    #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))]
517    pub(crate) fn create_timeout_listener(&self) -> TimeoutListener {
518        self.inner.do_create_timeout_listener()
519    }
520
521    // take needless &mut only to communicate its semantic mutability to humans...
522    #[cfg(feature = "dev-context-only-utils")]
523    pub fn drop_scheduler(&mut self) {
524        self.inner.drop_scheduler();
525    }
526
527    pub(crate) fn wait_for_paused_scheduler(bank: &Bank, scheduler: &InstalledSchedulerRwLock) {
528        let maybe_result_with_timings = BankWithSchedulerInner::wait_for_scheduler_termination(
529            bank,
530            scheduler,
531            WaitReason::PausedForRecentBlockhash,
532        );
533        assert!(
534            maybe_result_with_timings.is_none(),
535            "Premature result was returned from scheduler after paused (slot: {})",
536            bank.slot(),
537        );
538    }
539
540    #[must_use]
541    pub fn wait_for_completed_scheduler(&self) -> Option<ResultWithTimings> {
542        BankWithSchedulerInner::wait_for_scheduler_termination(
543            &self.inner.bank,
544            &self.inner.scheduler,
545            WaitReason::TerminatedToFreeze,
546        )
547    }
548
549    pub const fn no_scheduler_available() -> InstalledSchedulerRwLock {
550        RwLock::new(SchedulerStatus::Unavailable)
551    }
552}
553
554impl BankWithSchedulerInner {
555    fn with_active_scheduler(
556        self: &Arc<Self>,
557        f: impl FnOnce(&InstalledSchedulerBox) -> ScheduleResult,
558    ) -> ScheduleResult {
559        let scheduler = self.scheduler.read().unwrap();
560        match &*scheduler {
561            SchedulerStatus::Active(scheduler) => {
562                // This is the fast path, needing single read-lock most of time.
563                f(scheduler)
564            }
565            SchedulerStatus::Stale(_pool, (result, _timings)) if result.is_err() => {
566                trace!(
567                    "with_active_scheduler: bank (slot: {}) has a stale aborted scheduler...",
568                    self.bank.slot(),
569                );
570                Err(SchedulerAborted)
571            }
572            SchedulerStatus::Stale(pool, _result_with_timings) => {
573                let pool = pool.clone();
574                drop(scheduler);
575
576                // Schedulers can be stale only if its mode is block-verification. So,
577                // unconditional context construction for verification is okay here.
578                let context = SchedulingContext::new(self.bank.clone());
579                let mut scheduler = self.scheduler.write().unwrap();
580                trace!("with_active_scheduler: {scheduler:?}");
581                scheduler.transition_from_stale_to_active(|pool, result_with_timings| {
582                    // Re-taking a block verification scheduler should succeed because this code
583                    // path indicates taking it succeeded previously to begin with.
584                    // Note that a block production scheduler won't reach here because the whole
585                    // callback thing is gated by BankForks::install_scheduler_into_bank().
586                    let scheduler = pool
587                        .take_resumed_scheduler(context, result_with_timings)
588                        .unwrap();
589                    info!(
590                        "with_active_scheduler: bank (slot: {}) got active, taking scheduler (id: \
591                         {})",
592                        self.bank.slot(),
593                        scheduler.id(),
594                    );
595                    scheduler
596                });
597                drop(scheduler);
598
599                let scheduler = self.scheduler.read().unwrap();
600                // Re-register a new timeout listener only after acquiring the read lock;
601                // Otherwise, the listener would again put scheduler into Stale before the read
602                // lock under an extremely-rare race condition, causing panic below in
603                // active_scheduler().
604                pool.register_timeout_listener(self.do_create_timeout_listener());
605                f(scheduler.active_scheduler())
606            }
607            SchedulerStatus::Unavailable => {
608                unreachable!("no installed scheduler for slot {}", self.bank.slot())
609            }
610        }
611    }
612
613    fn do_create_timeout_listener(self: &Arc<Self>) -> TimeoutListener {
614        let weak_bank = Arc::downgrade(self);
615        TimeoutListener::new(move |pool| {
616            let Some(bank) = weak_bank.upgrade() else {
617                // BankWithSchedulerInner is already dropped, indicating successful and timely
618                // `wait_for_termination()` on the bank prior to this triggering of the timeout,
619                // rendering this callback invocation no-op.
620                return;
621            };
622
623            let Ok(mut scheduler) = bank.scheduler.write() else {
624                // BankWithScheduler's lock is poisoned...
625                return;
626            };
627
628            // Reaching here means that it's been awhile since this active scheduler is taken from
629            // the pool and yet it has yet to be `wait_for_termination()`-ed. To avoid unbounded
630            // thread creation under forky condition, return the scheduler for now, even if the
631            // bank could process more transactions later.
632            scheduler.maybe_transition_from_active_to_stale(|scheduler| {
633                // Return the installed scheduler back to the scheduler pool as soon as the
634                // scheduler indicates the completion of all currently-scheduled transaction
635                // executions by `solana_unified_scheduler_pool::ThreadManager::end_session()`
636                // internally.
637
638                let id = scheduler.id();
639                let (result_with_timings, uninstalled_scheduler) =
640                    scheduler.wait_for_termination(false);
641                uninstalled_scheduler.return_to_pool();
642                info!(
643                    "timeout_listener: bank (slot: {}) got stale, returning scheduler (id: {})",
644                    bank.bank.slot(),
645                    id,
646                );
647                (pool, result_with_timings)
648            });
649            trace!("timeout_listener: {scheduler:?}");
650        })
651    }
652
653    /// This must not be called until `Err(SchedulerAborted)` is observed. Violating this should
654    /// `panic!()`.
655    fn retrieve_error_after_schedule_failure(&self) -> TransactionError {
656        let mut scheduler = self.scheduler.write().unwrap();
657        match &mut *scheduler {
658            SchedulerStatus::Active(scheduler) => scheduler.recover_error_after_abort(),
659            SchedulerStatus::Stale(_pool, (result, _timings)) if result.is_err() => {
660                result.clone().unwrap_err()
661            }
662            _ => unreachable!("no error in {:?}", self.scheduler),
663        }
664    }
665
666    #[must_use]
667    fn wait_for_completed_scheduler_from_drop(&self) -> Option<ResultWithTimings> {
668        Self::wait_for_scheduler_termination(
669            &self.bank,
670            &self.scheduler,
671            WaitReason::DroppedFromBankForks,
672        )
673    }
674
675    #[must_use]
676    fn wait_for_scheduler_termination(
677        bank: &Bank,
678        scheduler: &InstalledSchedulerRwLock,
679        reason: WaitReason,
680    ) -> Option<ResultWithTimings> {
681        debug!(
682            "wait_for_scheduler_termination(slot: {}, reason: {:?}): started at {:?}...",
683            bank.slot(),
684            reason,
685            thread::current(),
686        );
687
688        let mut scheduler = scheduler.write().unwrap();
689        let (was_noop, result_with_timings) = match &mut *scheduler {
690            SchedulerStatus::Active(scheduler) if reason.is_paused() => {
691                scheduler.pause_for_recent_blockhash();
692                (false, None)
693            }
694            SchedulerStatus::Active(_scheduler) => {
695                let scheduler = scheduler.transition_from_active_to_unavailable();
696                let (result_with_timings, uninstalled_scheduler) =
697                    scheduler.wait_for_termination(reason.is_dropped());
698                uninstalled_scheduler.return_to_pool();
699                (false, Some(result_with_timings))
700            }
701            SchedulerStatus::Stale(_pool, _result_with_timings) if reason.is_paused() => {
702                // Do nothing for pauses because the scheduler termination is guaranteed to be
703                // called later.
704                (true, None)
705            }
706            SchedulerStatus::Stale(_pool, _result_with_timings) => {
707                let result_with_timings = scheduler.transition_from_stale_to_unavailable();
708                (true, Some(result_with_timings))
709            }
710            SchedulerStatus::Unavailable => (true, None),
711        };
712        debug!(
713            "wait_for_scheduler_termination(slot: {}, reason: {:?}): noop: {:?}, result: {:?} at \
714             {:?}...",
715            bank.slot(),
716            reason,
717            was_noop,
718            result_with_timings.as_ref().map(|(result, _)| result),
719            thread::current(),
720        );
721        trace!("wait_for_scheduler_termination(result_with_timings: {result_with_timings:?})",);
722
723        result_with_timings
724    }
725
726    fn drop_scheduler(&self) {
727        if thread::panicking() {
728            error!(
729                "BankWithSchedulerInner::drop_scheduler(): slot: {} skipping due to already \
730                 panicking...",
731                self.bank.slot(),
732            );
733            return;
734        }
735
736        // There's no guarantee ResultWithTimings is available or not at all when being dropped.
737        if let Some(Err(err)) = self
738            .wait_for_completed_scheduler_from_drop()
739            .map(|(result, _timings)| result)
740        {
741            warn!(
742                "BankWithSchedulerInner::drop_scheduler(): slot: {} discarding error from \
743                 scheduler: {:?}",
744                self.bank.slot(),
745                err,
746            );
747        }
748    }
749}
750
751impl Drop for BankWithSchedulerInner {
752    fn drop(&mut self) {
753        self.drop_scheduler();
754    }
755}
756
757impl Deref for BankWithScheduler {
758    type Target = Arc<Bank>;
759
760    fn deref(&self) -> &Self::Target {
761        &self.inner.bank
762    }
763}
764
765#[cfg(test)]
766mod tests {
767    use {
768        super::*,
769        crate::{
770            bank::test_utils::goto_end_of_slot_with_scheduler,
771            genesis_utils::{GenesisConfigInfo, create_genesis_config},
772        },
773        assert_matches::assert_matches,
774        mockall::Sequence,
775        solana_system_transaction as system_transaction,
776        std::sync::Mutex,
777    };
778
779    fn setup_mocked_scheduler_with_extra(
780        bank: Arc<Bank>,
781        is_dropped_flags: impl Iterator<Item = bool>,
782        f: Option<impl Fn(&mut MockInstalledScheduler)>,
783    ) -> InstalledSchedulerBox {
784        let mut mock = MockInstalledScheduler::new();
785        let seq = Arc::new(Mutex::new(Sequence::new()));
786
787        mock.expect_context()
788            .times(1)
789            .in_sequence(&mut seq.lock().unwrap())
790            .return_const(SchedulingContext::new(bank));
791
792        for wait_reason in is_dropped_flags {
793            let seq_cloned = seq.clone();
794            mock.expect_wait_for_termination()
795                .with(mockall::predicate::eq(wait_reason))
796                .times(1)
797                .in_sequence(&mut seq.lock().unwrap())
798                .returning(move |_| {
799                    let mut mock_uninstalled = MockUninstalledScheduler::new();
800                    mock_uninstalled
801                        .expect_return_to_pool()
802                        .times(1)
803                        .in_sequence(&mut seq_cloned.lock().unwrap())
804                        .returning(|| ());
805                    (
806                        (Ok(()), ExecuteTimings::default()),
807                        Box::new(mock_uninstalled),
808                    )
809                });
810        }
811
812        if let Some(f) = f {
813            f(&mut mock);
814        }
815
816        Box::new(mock)
817    }
818
819    fn setup_mocked_scheduler(
820        bank: Arc<Bank>,
821        is_dropped_flags: impl Iterator<Item = bool>,
822    ) -> InstalledSchedulerBox {
823        setup_mocked_scheduler_with_extra(
824            bank,
825            is_dropped_flags,
826            None::<fn(&mut MockInstalledScheduler) -> ()>,
827        )
828    }
829
830    #[test]
831    fn test_scheduler_normal_termination() {
832        agave_logger::setup();
833
834        let bank = Arc::new(Bank::default_for_tests());
835        let bank = BankWithScheduler::new(
836            bank.clone(),
837            Some(setup_mocked_scheduler(bank, [false].into_iter())),
838        );
839        assert!(bank.has_installed_scheduler());
840        assert_matches!(bank.wait_for_completed_scheduler(), Some(_));
841
842        // Repeating to call wait_for_completed_scheduler() is okay with no ResultWithTimings being
843        // returned.
844        assert!(!bank.has_installed_scheduler());
845        assert_matches!(bank.wait_for_completed_scheduler(), None);
846    }
847
848    #[test]
849    fn test_no_scheduler_termination() {
850        agave_logger::setup();
851
852        let bank = Arc::new(Bank::default_for_tests());
853        let bank = BankWithScheduler::new_without_scheduler(bank);
854
855        // Calling wait_for_completed_scheduler() is noop, when no scheduler is installed.
856        assert!(!bank.has_installed_scheduler());
857        assert_matches!(bank.wait_for_completed_scheduler(), None);
858    }
859
860    #[test]
861    fn test_scheduler_termination_from_drop() {
862        agave_logger::setup();
863
864        let bank = Arc::new(Bank::default_for_tests());
865        let bank = BankWithScheduler::new(
866            bank.clone(),
867            Some(setup_mocked_scheduler(bank, [true].into_iter())),
868        );
869        drop(bank);
870    }
871
872    #[test]
873    fn test_scheduler_pause() {
874        agave_logger::setup();
875
876        let bank = Arc::new(crate::bank::tests::create_simple_test_bank(42));
877        let bank = BankWithScheduler::new(
878            bank.clone(),
879            Some(setup_mocked_scheduler_with_extra(
880                bank,
881                [false].into_iter(),
882                Some(|mocked: &mut MockInstalledScheduler| {
883                    mocked
884                        .expect_pause_for_recent_blockhash()
885                        .times(1)
886                        .returning(|| ());
887                }),
888            )),
889        );
890        goto_end_of_slot_with_scheduler(&bank);
891        assert_matches!(bank.wait_for_completed_scheduler(), Some(_));
892    }
893
894    fn do_test_schedule_execution(should_succeed: bool) {
895        agave_logger::setup();
896
897        let GenesisConfigInfo {
898            genesis_config,
899            mint_keypair,
900            ..
901        } = create_genesis_config(10_000);
902        let tx0 = RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
903            &mint_keypair,
904            &solana_pubkey::new_rand(),
905            2,
906            genesis_config.hash(),
907        ));
908        let bank = Arc::new(Bank::new_for_tests(&genesis_config));
909        let mocked_scheduler = setup_mocked_scheduler_with_extra(
910            bank.clone(),
911            [true].into_iter(),
912            Some(|mocked: &mut MockInstalledScheduler| {
913                if should_succeed {
914                    mocked
915                        .expect_schedule_execution()
916                        .times(1)
917                        .returning(|_, _| Ok(()));
918                } else {
919                    mocked
920                        .expect_schedule_execution()
921                        .times(1)
922                        .returning(|_, _| Err(SchedulerAborted));
923                    mocked
924                        .expect_recover_error_after_abort()
925                        .times(1)
926                        .returning(|| TransactionError::InsufficientFundsForFee);
927                }
928            }),
929        );
930
931        let bank = BankWithScheduler::new(bank, Some(mocked_scheduler));
932        let result = bank.schedule_transaction_executions([(tx0, 0)].into_iter());
933        if should_succeed {
934            assert_matches!(result, Ok(()));
935        } else {
936            assert_matches!(result, Err(TransactionError::InsufficientFundsForFee));
937        }
938    }
939
940    #[test]
941    fn test_schedule_execution_success() {
942        do_test_schedule_execution(true);
943    }
944
945    #[test]
946    fn test_schedule_execution_failure() {
947        do_test_schedule_execution(false);
948    }
949}