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}