Skip to main content

clone_solana_unified_scheduler_pool/
lib.rs

1//! Transaction scheduling code.
2//!
3//! This crate implements 3 solana-runtime traits [`InstalledScheduler`], [`UninstalledScheduler`]
4//! and [`InstalledSchedulerPool`] to provide a concrete transaction scheduling implementation
5//! (including executing txes and committing tx results).
6//!
7//! At the highest level, this crate takes [`SanitizedTransaction`]s via its
8//! [`InstalledScheduler::schedule_execution`] and commits any side-effects (i.e. on-chain state
9//! changes) into the associated [`Bank`](clone_solana_runtime::bank::Bank) via `solana-ledger`'s helper
10//! function called [`execute_batch`].
11//!
12//! Refer to [`PooledScheduler`] doc comment for general overview of scheduler state transitions
13//! regarding to pooling and the actual use.
14
15#[cfg(feature = "dev-context-only-utils")]
16use qualifier_attr::qualifiers;
17use {
18    assert_matches::assert_matches,
19    clone_agave_banking_stage_ingress_types::{BankingPacketBatch, BankingPacketReceiver},
20    clone_solana_ledger::blockstore_processor::{
21        execute_batch, TransactionBatchWithIndexes, TransactionStatusSender,
22    },
23    clone_solana_poh::poh_recorder::{RecordTransactionsSummary, TransactionRecorder},
24    clone_solana_pubkey::Pubkey,
25    clone_solana_runtime::{
26        installed_scheduler_pool::{
27            initialized_result_with_timings, InstalledScheduler, InstalledSchedulerBox,
28            InstalledSchedulerPool, InstalledSchedulerPoolArc, ResultWithTimings, ScheduleResult,
29            SchedulerAborted, SchedulerId, SchedulingContext, TimeoutListener,
30            UninstalledScheduler, UninstalledSchedulerBox,
31        },
32        prioritization_fee_cache::PrioritizationFeeCache,
33        vote_sender_types::ReplayVoteSender,
34    },
35    clone_solana_runtime_transaction::runtime_transaction::RuntimeTransaction,
36    clone_solana_timings::ExecuteTimings,
37    clone_solana_transaction::sanitized::SanitizedTransaction,
38    clone_solana_transaction_error::{TransactionError, TransactionResult as Result},
39    clone_solana_unified_scheduler_logic::{
40        SchedulingMode::{self, BlockProduction, BlockVerification},
41        SchedulingStateMachine, Task, UsageQueue,
42    },
43    crossbeam_channel::{self, never, select_biased, Receiver, RecvError, SendError, Sender},
44    dashmap::DashMap,
45    derive_where::derive_where,
46    dyn_clone::{clone_trait_object, DynClone},
47    log::*,
48    scopeguard::defer,
49    static_assertions::const_assert_eq,
50    std::{
51        fmt::Debug,
52        marker::PhantomData,
53        mem,
54        sync::{
55            atomic::{AtomicU64, AtomicUsize, Ordering::Relaxed},
56            Arc, Mutex, OnceLock, Weak,
57        },
58        thread::{self, sleep, JoinHandle},
59        time::{Duration, Instant},
60    },
61    trait_set::trait_set,
62    vec_extract_if_polyfill::MakeExtractIf,
63};
64
65mod sleepless_testing;
66use crate::sleepless_testing::BuilderTracked;
67
68// dead_code is false positive; these tuple fields are used via Debug.
69#[allow(dead_code)]
70#[derive(Debug)]
71enum CheckPoint {
72    NewTask(usize),
73    TaskHandled(usize),
74    SchedulerThreadAborted,
75    IdleSchedulerCleaned(usize),
76    TrashedSchedulerCleaned(usize),
77    TimeoutListenerTriggered(usize),
78}
79
80type AtomicSchedulerId = AtomicU64;
81
82/// A pool of idling schedulers (usually [`PooledScheduler`]), ready to be taken by bank.
83///
84/// Also, the pool runs a _cleaner_ thread named as `solScCleaner`. its jobs include:
85///
86/// - Shrink of pool if there are too many idle schedulers.
87/// - Invocation of timeouts registered by [`InstalledSchedulerPool::register_timeout_listener`].
88/// - The actual destruction of any retired schedulers including thread termination and the heavy
89///   `UsageQueueLoader` drop.
90///
91/// `SchedulerPool` (and [`PooledScheduler`] in this regard) must be accessed as a dyn trait from
92/// `solana-runtime`, because it contains some internal fields, whose types aren't available in
93/// `solana-runtime` ( [`TransactionStatusSender`] and [`TransactionRecorder`]). Refer to the doc
94/// comment with a diagram at [`clone_solana_runtime::installed_scheduler_pool::InstalledScheduler`] for
95/// explanation of this rather complex dyn trait/type hierarchy.
96#[derive(Debug)]
97pub struct SchedulerPool<S: SpawnableScheduler<TH>, TH: TaskHandler> {
98    scheduler_inners: Mutex<Vec<(S::Inner, Instant)>>,
99    trashed_scheduler_inners: Mutex<Vec<S::Inner>>,
100    timeout_listeners: Mutex<Vec<(TimeoutListener, Instant)>>,
101    handler_count: usize,
102    common_handler_context: CommonHandlerContext,
103    banking_stage_handler_context: Mutex<Option<BankingStageHandlerContext>>,
104    // weak_self could be elided by changing InstalledScheduler::take_scheduler()'s receiver to
105    // Arc<Self> from &Self, because SchedulerPool is used as in the form of Arc<SchedulerPool>
106    // almost always. But, this would cause wasted and noisy Arc::clone()'s at every call sites.
107    //
108    // Alternatively, `impl InstalledScheduler for Arc<SchedulerPool>` approach could be explored
109    // but it entails its own problems due to rustc's coherence and necessitated newtype with the
110    // type graph of InstalledScheduler being quite elaborate.
111    //
112    // After these considerations, this weak_self approach is chosen at the cost of some additional
113    // memory increase.
114    weak_self: Weak<Self>,
115    next_scheduler_id: AtomicSchedulerId,
116    max_usage_queue_count: usize,
117    _phantom: PhantomData<TH>,
118}
119
120#[derive(derive_more::Debug, Clone)]
121pub struct HandlerContext {
122    log_messages_bytes_limit: Option<usize>,
123    transaction_status_sender: Option<TransactionStatusSender>,
124    replay_vote_sender: Option<ReplayVoteSender>,
125    prioritization_fee_cache: Arc<PrioritizationFeeCache>,
126    banking_packet_receiver: BankingPacketReceiver,
127    #[debug("{banking_packet_handler:p}")]
128    banking_packet_handler: Box<dyn BankingPacketHandler>,
129    banking_stage_helper: Option<Arc<BankingStageHelper>>,
130    transaction_recorder: Option<TransactionRecorder>,
131}
132
133#[derive(Debug, Clone)]
134struct CommonHandlerContext {
135    log_messages_bytes_limit: Option<usize>,
136    transaction_status_sender: Option<TransactionStatusSender>,
137    replay_vote_sender: Option<ReplayVoteSender>,
138    prioritization_fee_cache: Arc<PrioritizationFeeCache>,
139}
140
141impl CommonHandlerContext {
142    fn into_handler_context(
143        self,
144        banking_packet_receiver: BankingPacketReceiver,
145        banking_packet_handler: Box<dyn BankingPacketHandler>,
146        banking_stage_helper: Option<Arc<BankingStageHelper>>,
147        transaction_recorder: Option<TransactionRecorder>,
148    ) -> HandlerContext {
149        let Self {
150            log_messages_bytes_limit,
151            transaction_status_sender,
152            replay_vote_sender,
153            prioritization_fee_cache,
154        } = self;
155
156        HandlerContext {
157            log_messages_bytes_limit,
158            transaction_status_sender,
159            replay_vote_sender,
160            prioritization_fee_cache,
161            banking_packet_receiver,
162            banking_packet_handler,
163            banking_stage_helper,
164            transaction_recorder,
165        }
166    }
167}
168
169#[derive(derive_more::Debug)]
170struct BankingStageHandlerContext {
171    banking_packet_receiver: BankingPacketReceiver,
172    #[debug("{banking_packet_handler:p}")]
173    banking_packet_handler: Box<dyn BankingPacketHandler>,
174    transaction_recorder: TransactionRecorder,
175}
176
177trait_set! {
178    pub trait BankingPacketHandler =
179        DynClone + FnMut(&BankingStageHelper, BankingPacketBatch) + Send + 'static;
180}
181// Make this `Clone`-able so that it can easily propagated to all the handler threads.
182clone_trait_object!(BankingPacketHandler);
183
184#[derive(Debug)]
185pub struct BankingStageHelper {
186    usage_queue_loader: UsageQueueLoader,
187    next_task_id: AtomicUsize,
188    new_task_sender: Sender<NewTaskPayload>,
189}
190
191impl BankingStageHelper {
192    fn new(new_task_sender: Sender<NewTaskPayload>) -> Self {
193        Self {
194            usage_queue_loader: UsageQueueLoader::default(),
195            next_task_id: AtomicUsize::default(),
196            new_task_sender,
197        }
198    }
199
200    pub fn generate_task_ids(&self, count: usize) -> usize {
201        self.next_task_id.fetch_add(count, Relaxed)
202    }
203
204    pub fn create_new_task(
205        &self,
206        transaction: RuntimeTransaction<SanitizedTransaction>,
207        index: usize,
208    ) -> Task {
209        SchedulingStateMachine::create_task(transaction, index, &mut |pubkey| {
210            self.usage_queue_loader.load(pubkey)
211        })
212    }
213
214    pub fn send_new_task(&self, task: Task) {
215        self.new_task_sender
216            .send(NewTaskPayload::Payload(task))
217            .unwrap();
218    }
219}
220
221pub type DefaultSchedulerPool =
222    SchedulerPool<PooledScheduler<DefaultTaskHandler>, DefaultTaskHandler>;
223
224const DEFAULT_POOL_CLEANER_INTERVAL: Duration = Duration::from_secs(10);
225const DEFAULT_MAX_POOLING_DURATION: Duration = Duration::from_secs(180);
226const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(12);
227// Rough estimate of max UsageQueueLoader size in bytes:
228//   UsageFromTask * UsageQueue's capacity * DEFAULT_MAX_USAGE_QUEUE_COUNT
229//   16 bytes      * 128 items             * 262_144 entries               == 512 MiB
230// It's expected that there will be 2 or 3 pooled schedulers constantly when running against
231// mainnnet-beta. That means the total memory consumption for the idle close-to-be-trashed pooled
232// schedulers is set to 1.0 ~ 1.5 GiB. This value is chosen to maximize performance under the
233// normal cluster condition to avoid memory reallocation as much as possible. That said, it's not
234// likely this would allow unbounded memory growth when the cluster is unstable or under some kind
235// of attacks. That's because this limit is enforced at every slot and the UsageQueueLoader itself
236// is recreated without any entries at first, needing to repopulate by means of actual use to eat
237// the memory.
238//
239// Along the lines, this isn't problematic for the development settings (= solana-test-validator),
240// because UsageQueueLoader won't grow that much to begin with.
241const DEFAULT_MAX_USAGE_QUEUE_COUNT: usize = 262_144;
242
243impl<S, TH> SchedulerPool<S, TH>
244where
245    S: SpawnableScheduler<TH>,
246    TH: TaskHandler,
247{
248    // Some internal impl and test code want an actual concrete type, NOT the
249    // `dyn InstalledSchedulerPool`. So don't merge this into `Self::new_dyn()`.
250    #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))]
251    fn new(
252        handler_count: Option<usize>,
253        log_messages_bytes_limit: Option<usize>,
254        transaction_status_sender: Option<TransactionStatusSender>,
255        replay_vote_sender: Option<ReplayVoteSender>,
256        prioritization_fee_cache: Arc<PrioritizationFeeCache>,
257    ) -> Arc<Self> {
258        Self::do_new(
259            handler_count,
260            log_messages_bytes_limit,
261            transaction_status_sender,
262            replay_vote_sender,
263            prioritization_fee_cache,
264            DEFAULT_POOL_CLEANER_INTERVAL,
265            DEFAULT_MAX_POOLING_DURATION,
266            DEFAULT_MAX_USAGE_QUEUE_COUNT,
267            DEFAULT_TIMEOUT_DURATION,
268        )
269    }
270
271    fn do_new(
272        handler_count: Option<usize>,
273        log_messages_bytes_limit: Option<usize>,
274        transaction_status_sender: Option<TransactionStatusSender>,
275        replay_vote_sender: Option<ReplayVoteSender>,
276        prioritization_fee_cache: Arc<PrioritizationFeeCache>,
277        pool_cleaner_interval: Duration,
278        max_pooling_duration: Duration,
279        max_usage_queue_count: usize,
280        timeout_duration: Duration,
281    ) -> Arc<Self> {
282        let handler_count = handler_count.unwrap_or(Self::default_handler_count());
283        assert!(handler_count >= 1);
284
285        let scheduler_pool = Arc::new_cyclic(|weak_self| Self {
286            scheduler_inners: Mutex::default(),
287            trashed_scheduler_inners: Mutex::default(),
288            timeout_listeners: Mutex::default(),
289            handler_count,
290            common_handler_context: CommonHandlerContext {
291                log_messages_bytes_limit,
292                transaction_status_sender,
293                replay_vote_sender,
294                prioritization_fee_cache,
295            },
296            banking_stage_handler_context: Mutex::default(),
297            weak_self: weak_self.clone(),
298            next_scheduler_id: AtomicSchedulerId::default(),
299            max_usage_queue_count,
300            _phantom: PhantomData,
301        });
302
303        let cleaner_main_loop = {
304            let weak_scheduler_pool = Arc::downgrade(&scheduler_pool);
305
306            move || loop {
307                sleep(pool_cleaner_interval);
308
309                let Some(scheduler_pool) = weak_scheduler_pool.upgrade() else {
310                    break;
311                };
312
313                let now = Instant::now();
314
315                let idle_inner_count = {
316                    // Pre-allocate rather large capacity to avoid reallocation inside the lock.
317                    let mut idle_inners = Vec::with_capacity(128);
318
319                    let Ok(mut scheduler_inners) = scheduler_pool.scheduler_inners.lock() else {
320                        break;
321                    };
322                    // Use the still-unstable Vec::extract_if() even on stable rust toolchain by
323                    // using a polyfill and allowing unstable_name_collisions, because it's
324                    // simplest to code and fastest to run (= O(n); single linear pass and no
325                    // reallocation).
326                    //
327                    // Note that this critical section could block the latency-sensitive replay
328                    // code-path via ::take_scheduler().
329                    #[allow(unstable_name_collisions)]
330                    idle_inners.extend(scheduler_inners.extract_if(|(_inner, pooled_at)| {
331                        now.duration_since(*pooled_at) > max_pooling_duration
332                    }));
333                    drop(scheduler_inners);
334
335                    let idle_inner_count = idle_inners.len();
336                    drop(idle_inners);
337                    idle_inner_count
338                };
339
340                let trashed_inner_count = {
341                    let Ok(mut trashed_scheduler_inners) =
342                        scheduler_pool.trashed_scheduler_inners.lock()
343                    else {
344                        break;
345                    };
346                    let trashed_inners: Vec<_> = mem::take(&mut *trashed_scheduler_inners);
347                    drop(trashed_scheduler_inners);
348
349                    let trashed_inner_count = trashed_inners.len();
350                    drop(trashed_inners);
351                    trashed_inner_count
352                };
353
354                let triggered_timeout_listener_count = {
355                    // Pre-allocate rather large capacity to avoid reallocation inside the lock.
356                    let mut expired_listeners = Vec::with_capacity(128);
357                    let Ok(mut timeout_listeners) = scheduler_pool.timeout_listeners.lock() else {
358                        break;
359                    };
360                    #[allow(unstable_name_collisions)]
361                    expired_listeners.extend(timeout_listeners.extract_if(
362                        |(_callback, registered_at)| {
363                            now.duration_since(*registered_at) > timeout_duration
364                        },
365                    ));
366                    drop(timeout_listeners);
367
368                    let count = expired_listeners.len();
369                    // Now triggers all expired listeners. Usually, triggering timeouts does
370                    // nothing because the callbacks will be no-op if already successfully
371                    // `wait_for_termination()`-ed.
372                    for (timeout_listener, _registered_at) in expired_listeners {
373                        timeout_listener.trigger(scheduler_pool.clone());
374                    }
375                    count
376                };
377
378                info!(
379                    "Scheduler pool cleaner: dropped {} idle inners, {} trashed inners, triggered {} timeout listeners",
380                    idle_inner_count, trashed_inner_count, triggered_timeout_listener_count,
381                );
382                sleepless_testing::at(CheckPoint::IdleSchedulerCleaned(idle_inner_count));
383                sleepless_testing::at(CheckPoint::TrashedSchedulerCleaned(trashed_inner_count));
384                sleepless_testing::at(CheckPoint::TimeoutListenerTriggered(
385                    triggered_timeout_listener_count,
386                ));
387            }
388        };
389
390        // No need to join; the spawned main loop will gracefully exit.
391        thread::Builder::new()
392            .name("solScCleaner".to_owned())
393            .spawn_tracked(cleaner_main_loop)
394            .unwrap();
395
396        scheduler_pool
397    }
398
399    // This apparently-meaningless wrapper is handy, because some callers explicitly want
400    // `dyn InstalledSchedulerPool` to be returned for type inference convenience.
401    pub fn new_dyn(
402        handler_count: Option<usize>,
403        log_messages_bytes_limit: Option<usize>,
404        transaction_status_sender: Option<TransactionStatusSender>,
405        replay_vote_sender: Option<ReplayVoteSender>,
406        prioritization_fee_cache: Arc<PrioritizationFeeCache>,
407    ) -> InstalledSchedulerPoolArc {
408        Self::new(
409            handler_count,
410            log_messages_bytes_limit,
411            transaction_status_sender,
412            replay_vote_sender,
413            prioritization_fee_cache,
414        )
415    }
416
417    // See a comment at the weak_self field for justification of this method's existence.
418    fn self_arc(&self) -> Arc<Self> {
419        self.weak_self
420            .upgrade()
421            .expect("self-referencing Arc-ed pool")
422    }
423
424    fn new_scheduler_id(&self) -> SchedulerId {
425        self.next_scheduler_id.fetch_add(1, Relaxed)
426    }
427
428    // This fn needs to return immediately due to being part of the blocking
429    // `::wait_for_termination()` call.
430    fn return_scheduler(&self, scheduler: S::Inner) {
431        // Refer to the comment in is_aborted() as to the exact definition of the concept of
432        // _trashed_ and the interaction among different parts of unified scheduler.
433        let should_trash = scheduler.is_trashed();
434        if should_trash {
435            // Delay drop()-ing this trashed returned scheduler inner by stashing it in
436            // self.trashed_scheduler_inners, which is periodically drained by the `solScCleaner`
437            // thread. Dropping it could take long time (in fact,
438            // PooledSchedulerInner::usage_queue_loader can contain many entries to drop).
439            self.trashed_scheduler_inners
440                .lock()
441                .expect("not poisoned")
442                .push(scheduler);
443        } else {
444            self.scheduler_inners
445                .lock()
446                .expect("not poisoned")
447                .push((scheduler, Instant::now()));
448        }
449    }
450
451    #[cfg(test)]
452    fn do_take_scheduler(&self, context: SchedulingContext) -> S {
453        self.do_take_resumed_scheduler(context, initialized_result_with_timings())
454    }
455
456    fn do_take_resumed_scheduler(
457        &self,
458        context: SchedulingContext,
459        result_with_timings: ResultWithTimings,
460    ) -> S {
461        assert_matches!(result_with_timings, (Ok(_), _));
462
463        // pop is intentional for filo, expecting relatively warmed-up scheduler due to having been
464        // returned recently
465        if let Some((inner, _pooled_at)) = self.scheduler_inners.lock().expect("not poisoned").pop()
466        {
467            S::from_inner(inner, context, result_with_timings)
468        } else {
469            S::spawn(self.self_arc(), context, result_with_timings)
470        }
471    }
472
473    #[cfg(feature = "dev-context-only-utils")]
474    pub fn pooled_scheduler_count(&self) -> usize {
475        self.scheduler_inners.lock().expect("not poisoned").len()
476    }
477
478    pub fn register_banking_stage(
479        &self,
480        banking_packet_receiver: BankingPacketReceiver,
481        banking_packet_handler: Box<dyn BankingPacketHandler>,
482        transaction_recorder: TransactionRecorder,
483    ) {
484        *self.banking_stage_handler_context.lock().unwrap() = Some(BankingStageHandlerContext {
485            banking_packet_receiver,
486            banking_packet_handler,
487            transaction_recorder,
488        });
489    }
490
491    fn create_handler_context(
492        &self,
493        mode: SchedulingMode,
494        new_task_sender: &Sender<NewTaskPayload>,
495    ) -> HandlerContext {
496        let (
497            banking_packet_receiver,
498            banking_packet_handler,
499            banking_stage_helper,
500            transaction_recorder,
501        ): (
502            _,
503            Box<dyn BankingPacketHandler>, /* to aid type inference */
504            _,
505            _,
506        ) = match mode {
507            BlockVerification => {
508                // Return various type-specific no-op values.
509                (never(), Box::new(|_, _| {}), None, None)
510            }
511            BlockProduction => {
512                let handler_context = self.banking_stage_handler_context.lock().unwrap();
513                let handler_context = handler_context.as_ref().unwrap();
514
515                (
516                    handler_context.banking_packet_receiver.clone(),
517                    handler_context.banking_packet_handler.clone(),
518                    Some(Arc::new(BankingStageHelper::new(new_task_sender.clone()))),
519                    Some(handler_context.transaction_recorder.clone()),
520                )
521            }
522        };
523        self.common_handler_context.clone().into_handler_context(
524            banking_packet_receiver,
525            banking_packet_handler,
526            banking_stage_helper,
527            transaction_recorder,
528        )
529    }
530
531    pub fn default_handler_count() -> usize {
532        Self::calculate_default_handler_count(
533            thread::available_parallelism()
534                .ok()
535                .map(|non_zero| non_zero.get()),
536        )
537    }
538
539    pub fn calculate_default_handler_count(detected_cpu_core_count: Option<usize>) -> usize {
540        // Divide by 4 just not to consume all available CPUs just with handler threads, sparing for
541        // other active forks and other subsystems.
542        // Also, if available_parallelism fails (which should be very rare), use 4 threads,
543        // as a relatively conservatism assumption of modern multi-core systems ranging from
544        // engineers' laptops to production servers.
545        detected_cpu_core_count
546            .map(|core_count| (core_count / 4).max(1))
547            .unwrap_or(4)
548    }
549
550    pub fn cli_message() -> &'static str {
551        static MESSAGE: OnceLock<String> = OnceLock::new();
552
553        MESSAGE.get_or_init(|| {
554            format!(
555                "Change the number of the unified scheduler's transaction execution threads \
556                 dedicated to each block, otherwise calculated as cpu_cores/4 [default: {}]",
557                Self::default_handler_count()
558            )
559        })
560    }
561}
562
563impl<S, TH> InstalledSchedulerPool for SchedulerPool<S, TH>
564where
565    S: SpawnableScheduler<TH>,
566    TH: TaskHandler,
567{
568    fn take_resumed_scheduler(
569        &self,
570        context: SchedulingContext,
571        result_with_timings: ResultWithTimings,
572    ) -> InstalledSchedulerBox {
573        Box::new(self.do_take_resumed_scheduler(context, result_with_timings))
574    }
575
576    fn register_timeout_listener(&self, timeout_listener: TimeoutListener) {
577        self.timeout_listeners
578            .lock()
579            .unwrap()
580            .push((timeout_listener, Instant::now()));
581    }
582}
583
584pub trait TaskHandler: Send + Sync + Debug + Sized + 'static {
585    fn handle(
586        result: &mut Result<()>,
587        timings: &mut ExecuteTimings,
588        scheduling_context: &SchedulingContext,
589        task: &Task,
590        handler_context: &HandlerContext,
591    );
592}
593
594#[derive(Debug)]
595pub struct DefaultTaskHandler;
596
597impl TaskHandler for DefaultTaskHandler {
598    fn handle(
599        result: &mut Result<()>,
600        timings: &mut ExecuteTimings,
601        scheduling_context: &SchedulingContext,
602        task: &Task,
603        handler_context: &HandlerContext,
604    ) {
605        // scheduler must properly prevent conflicting tx executions. thus, task handler isn't
606        // responsible for locking.
607        let bank = scheduling_context.bank();
608        let transaction = task.transaction();
609        let index = task.task_index();
610
611        let batch = bank.prepare_unlocked_batch_from_single_tx(transaction);
612        let transaction_indexes = match scheduling_context.mode() {
613            BlockVerification => vec![index],
614            BlockProduction => {
615                // Create a placeholder vec, which will be populated later if
616                // transaction_status_sender is Some(_).
617                // transaction_status_sender is usually None for staked nodes because it's only
618                // used for RPC-related additional data recording. However, a staked node could
619                // also be running with rpc functionalities during development. So, we need to
620                // correctly support the use case for produced blocks as well, like verified blocks
621                // via the replaying stage.
622                // Refer `record_token_balances` in `execute_batch()` as this treatment is mirrored
623                // from it.
624                vec![]
625            }
626        };
627        let batch_with_indexes = TransactionBatchWithIndexes {
628            batch,
629            transaction_indexes,
630        };
631
632        let pre_commit_callback = match scheduling_context.mode() {
633            BlockVerification => None,
634            BlockProduction => Some(|processing_result: &'_ Result<_>| {
635                if let Err(error) = processing_result {
636                    Err(error.clone())?;
637                };
638
639                let RecordTransactionsSummary {
640                    result,
641                    starting_transaction_index,
642                    ..
643                } = handler_context
644                    .transaction_recorder
645                    .as_ref()
646                    .unwrap()
647                    .record_transactions(bank.slot(), vec![transaction.to_versioned_transaction()]);
648                match result {
649                    Ok(()) => Ok(starting_transaction_index),
650                    Err(_) => Err(TransactionError::CommitCancelled),
651                }
652            }),
653        };
654
655        *result = execute_batch(
656            &batch_with_indexes,
657            bank,
658            handler_context.transaction_status_sender.as_ref(),
659            handler_context.replay_vote_sender.as_ref(),
660            timings,
661            handler_context.log_messages_bytes_limit,
662            &handler_context.prioritization_fee_cache,
663            pre_commit_callback,
664        );
665        sleepless_testing::at(CheckPoint::TaskHandled(index));
666    }
667}
668
669struct ExecutedTask {
670    task: Task,
671    result_with_timings: ResultWithTimings,
672}
673
674impl ExecutedTask {
675    fn new_boxed(task: Task) -> Box<Self> {
676        Box::new(Self {
677            task,
678            result_with_timings: initialized_result_with_timings(),
679        })
680    }
681}
682
683// A very tiny generic message type to signal about opening and closing of subchannels, which are
684// logically segmented series of Payloads (P1) over a single continuous time-span, potentially
685// carrying some subchannel metadata (P2) upon opening a new subchannel.
686// Note that the above properties can be upheld only when this is used inside MPSC or SPSC channels
687// (i.e. the consumer side needs to be single threaded). For the multiple consumer cases,
688// ChainedChannel can be used instead.
689enum SubchanneledPayload<P1, P2> {
690    Payload(P1),
691    OpenSubchannel(P2),
692    CloseSubchannel,
693}
694
695type NewTaskPayload = SubchanneledPayload<Task, Box<(SchedulingContext, ResultWithTimings)>>;
696const_assert_eq!(mem::size_of::<NewTaskPayload>(), 16);
697
698// A tiny generic message type to synchronize multiple threads everytime some contextual data needs
699// to be switched (ie. SchedulingContext), just using a single communication channel.
700//
701// Usually, there's no way to prevent one of those threads from mixing current and next contexts
702// while processing messages with a multiple-consumer channel. A condvar or other
703// out-of-bound mechanism is needed to notify about switching of contextual data. That's because
704// there's no way to block those threads reliably on such a switching event just with a channel.
705//
706// However, if the number of consumer can be determined, this can be accomplished just over a
707// single channel, which even carries an in-bound control meta-message with the contexts. The trick
708// is that identical meta-messages as many as the number of threads are sent over the channel,
709// along with new channel receivers to be used (hence the name of _chained_). Then, the receiving
710// thread drops the old channel and is now blocked on receiving from the new channel. In this way,
711// this switching can happen exactly once for each thread.
712//
713// Overall, this greatly simplifies the code, reduces CAS/syscall overhead per messaging to the
714// minimum at the cost of a single channel recreation per switching. Needless to say, such an
715// allocation can be amortized to be negligible.
716//
717// Lastly, there's an auxiliary channel to realize a 2-level priority queue. See comment before
718// runnable_task_sender.
719mod chained_channel {
720    use super::*;
721
722    // hide variants by putting this inside newtype
723    enum ChainedChannelPrivate<P, C> {
724        Payload(P),
725        ContextAndChannels(C, Receiver<ChainedChannel<P, C>>, Receiver<P>),
726    }
727
728    pub(super) struct ChainedChannel<P, C>(ChainedChannelPrivate<P, C>);
729
730    impl<P, C> ChainedChannel<P, C> {
731        fn chain_to_new_channel(
732            context: C,
733            receiver: Receiver<Self>,
734            aux_receiver: Receiver<P>,
735        ) -> Self {
736            Self(ChainedChannelPrivate::ContextAndChannels(
737                context,
738                receiver,
739                aux_receiver,
740            ))
741        }
742    }
743
744    pub(super) struct ChainedChannelSender<P, C> {
745        sender: Sender<ChainedChannel<P, C>>,
746        aux_sender: Sender<P>,
747    }
748
749    impl<P, C: Clone> ChainedChannelSender<P, C> {
750        fn new(sender: Sender<ChainedChannel<P, C>>, aux_sender: Sender<P>) -> Self {
751            Self { sender, aux_sender }
752        }
753
754        pub(super) fn send_payload(
755            &self,
756            payload: P,
757        ) -> std::result::Result<(), SendError<ChainedChannel<P, C>>> {
758            self.sender
759                .send(ChainedChannel(ChainedChannelPrivate::Payload(payload)))
760        }
761
762        pub(super) fn send_aux_payload(&self, payload: P) -> std::result::Result<(), SendError<P>> {
763            self.aux_sender.send(payload)
764        }
765
766        pub(super) fn send_chained_channel(
767            &mut self,
768            context: &C,
769            count: usize,
770        ) -> std::result::Result<(), SendError<ChainedChannel<P, C>>> {
771            let (chained_sender, chained_receiver) = crossbeam_channel::unbounded();
772            let (chained_aux_sender, chained_aux_receiver) = crossbeam_channel::unbounded();
773            for _ in 0..count {
774                self.sender.send(ChainedChannel::chain_to_new_channel(
775                    context.clone(),
776                    chained_receiver.clone(),
777                    chained_aux_receiver.clone(),
778                ))?
779            }
780            self.sender = chained_sender;
781            self.aux_sender = chained_aux_sender;
782            Ok(())
783        }
784    }
785
786    // P doesn't need to be `: Clone`, yet rustc derive can't handle it.
787    // see https://github.com/rust-lang/rust/issues/26925
788    #[derive_where(Clone)]
789    pub(super) struct ChainedChannelReceiver<P, C: Clone> {
790        receiver: Receiver<ChainedChannel<P, C>>,
791        aux_receiver: Receiver<P>,
792        context: C,
793    }
794
795    impl<P, C: Clone> ChainedChannelReceiver<P, C> {
796        fn new(
797            receiver: Receiver<ChainedChannel<P, C>>,
798            aux_receiver: Receiver<P>,
799            initial_context: C,
800        ) -> Self {
801            Self {
802                receiver,
803                aux_receiver,
804                context: initial_context,
805            }
806        }
807
808        pub(super) fn context(&self) -> &C {
809            &self.context
810        }
811
812        pub(super) fn for_select(&self) -> &Receiver<ChainedChannel<P, C>> {
813            &self.receiver
814        }
815
816        pub(super) fn aux_for_select(&self) -> &Receiver<P> {
817            &self.aux_receiver
818        }
819
820        pub(super) fn never_receive_from_aux(&mut self) {
821            self.aux_receiver = never();
822        }
823
824        pub(super) fn after_select(&mut self, message: ChainedChannel<P, C>) -> Option<P> {
825            match message.0 {
826                ChainedChannelPrivate::Payload(payload) => Some(payload),
827                ChainedChannelPrivate::ContextAndChannels(context, channel, idle_channel) => {
828                    self.context = context;
829                    self.receiver = channel;
830                    self.aux_receiver = idle_channel;
831                    None
832                }
833            }
834        }
835    }
836
837    pub(super) fn unbounded<P, C: Clone>(
838        initial_context: C,
839    ) -> (ChainedChannelSender<P, C>, ChainedChannelReceiver<P, C>) {
840        let (sender, receiver) = crossbeam_channel::unbounded();
841        let (aux_sender, aux_receiver) = crossbeam_channel::unbounded();
842        (
843            ChainedChannelSender::new(sender, aux_sender),
844            ChainedChannelReceiver::new(receiver, aux_receiver, initial_context),
845        )
846    }
847}
848
849/// The primary owner of all [`UsageQueue`]s used for particular [`PooledScheduler`].
850///
851/// Currently, the simplest implementation. This grows memory usage in unbounded way. Overgrown
852/// instance destruction is managed via `solScCleaner`. This struct is here to be put outside
853/// `solana-unified-scheduler-logic` for the crate's original intent (separation of concerns from
854/// the pure-logic-only crate). Some practical and mundane pruning will be implemented in this type.
855#[derive(Default, Debug)]
856pub struct UsageQueueLoader {
857    usage_queues: DashMap<Pubkey, UsageQueue>,
858}
859
860impl UsageQueueLoader {
861    pub fn load(&self, address: Pubkey) -> UsageQueue {
862        self.usage_queues.entry(address).or_default().clone()
863    }
864
865    fn count(&self) -> usize {
866        self.usage_queues.len()
867    }
868}
869
870// (this is slow needing atomic mem reads. However, this can be turned into a lot faster
871// optimizer-friendly version as shown in this crossbeam pr:
872// https://github.com/crossbeam-rs/crossbeam/pull/1047)
873fn disconnected<T>() -> Receiver<T> {
874    // drop the sender residing at .0, returning an always-disconnected receiver.
875    crossbeam_channel::unbounded().1
876}
877
878#[cfg_attr(doc, aquamarine::aquamarine)]
879/// The concrete scheduler instance along with 1 scheduler and N handler threads.
880///
881/// This implements the dyn-compatible [`InstalledScheduler`] trait to be interacted by
882/// solana-runtime code as `Box<dyn _>`.  This also implements the [`SpawnableScheduler`] subtrait
883/// to be spawned and pooled by [`SchedulerPool`].  When a scheduler is said to be _taken_ from a
884/// pool, the Rust's ownership is literally moved from the pool's vec to the particular
885/// [`BankWithScheduler`](clone_solana_runtime::installed_scheduler_pool::BankWithScheduler) for
886/// type-level protection against double-use by different banks. As soon as the bank is
887/// [`is_complete()`](`clone_solana_runtime::bank::Bank::is_complete`) (i.e. ready for freezing), the
888/// associated scheduler is immediately _returned_ to the pool via
889/// [`InstalledScheduler::wait_for_termination`], to be taken by other banks quickly (usually,
890/// child bank).
891///
892/// Pooling is implemented to avoid repeated thread creation/destruction. Further more, each
893/// scheduler should manage its own set of threads, to be independent from other scheduler's
894/// threads for concurrent and efficient processing of banks of different forks.
895///
896/// It's intentionally designed for a start and end of scheduler use by banks not to incur any
897/// heavy system resource manipulation to reduce the latency of this per-block bookkeeping as much
898/// as possible.
899///
900/// To complement the above most common situation, there's various erroneous conditions: timeouts
901/// and abortions.
902///
903/// Timeouts are for rare conditions where there are abandoned-yet-unpruned banks in the
904/// [`BankForks`](clone_solana_runtime::bank_forks::BankForks) under forky (unsteady rooting) cluster
905/// conditions. The pool's background cleaner thread (`solScCleaner`) triggers the timeout-based
906/// out-of-pool (i.e. _taken_) scheduler reclaimation with prior coordination of
907/// [`BankForks::insert()`](clone_solana_runtime::bank_forks::BankForks::insert) via
908/// [`InstalledSchedulerPool::register_timeout_listener`].
909///
910/// Abortions are for another rate conditions where there's a fatal processing error, marking the
911/// given block as dead. In this case, all threads are terminated abruptly as much as possible to
912/// avoid any further system resource consumption on this possibly malice block. This error
913/// condition can implicitly be signalled to the replay stage on further transaction scheduling or
914/// can explicitly be done so on the eventual `wait_for_termination()` by drops or timeouts.
915///
916/// Lastly, scheduler can finally be _retired_ to be ready for thread termination due to various
917/// reasons like [`UsageQueueLoader`] being overgrown or many idling schedulers in the pool, in
918/// addition to the obvious reason of aborted scheduler.
919///
920/// ### Life cycle and ownership movement across crates of a particular scheduler
921///
922/// ```mermaid
923/// stateDiagram-v2
924///     [*] --> Active: Spawned (New bank by solReplayStage)
925///     state solana-runtime {
926///         state if_usable <<choice>>
927///         Active --> if_usable: Returned (Bank-freezing by solReplayStage)
928///         Active --> if_usable: Dropped (BankForks-pruning by solReplayStage)
929///         Aborted --> if_usable: Dropped (BankForks-pruning by solReplayStage)
930///         if_usable --> Pooled: IF !overgrown && !aborted
931///         Active --> Aborted: Errored on TX execution
932///         Aborted --> Stale: !Droppped after TIMEOUT_DURATION since taken
933///         Active --> Stale: No new TX after TIMEOUT_DURATION since taken
934///         Stale --> if_usable: Returned (Timeout-triggered by solScCleaner)
935///         Pooled --> Active: Taken (New bank by solReplayStage)
936///     }
937///     state solana-unified-scheduler-pool {
938///         Pooled --> Idle: !Taken after POOLING_DURATION
939///         if_usable --> Trashed: IF overgrown || aborted
940///         Idle --> Retired
941///         Trashed --> Retired
942///     }
943///     Retired --> [*]: Terminated (by solScCleaner)
944/// ```
945#[derive(Debug)]
946pub struct PooledScheduler<TH: TaskHandler> {
947    inner: PooledSchedulerInner<Self, TH>,
948    context: SchedulingContext,
949}
950
951#[derive(Debug)]
952pub struct PooledSchedulerInner<S: SpawnableScheduler<TH>, TH: TaskHandler> {
953    thread_manager: ThreadManager<S, TH>,
954    usage_queue_loader: UsageQueueLoader,
955}
956
957impl<S, TH> Drop for ThreadManager<S, TH>
958where
959    S: SpawnableScheduler<TH>,
960    TH: TaskHandler,
961{
962    fn drop(&mut self) {
963        trace!("ThreadManager::drop() is called...");
964
965        if self.are_threads_joined() {
966            return;
967        }
968        // If on-stack ThreadManager is being dropped abruptly while panicking, it's likely
969        // ::into_inner() isn't called, which is a critical runtime invariant for the following
970        // thread shutdown. Also, the state could be corrupt in other ways too, so just skip it
971        // altogether.
972        if thread::panicking() {
973            error!(
974                "ThreadManager::drop(): scheduler_id: {} skipping due to already panicking...",
975                self.scheduler_id,
976            );
977            return;
978        }
979
980        // assert that this is called after ::into_inner()
981        assert_matches!(self.session_result_with_timings, None);
982
983        // Ensure to initiate thread shutdown via disconnected new_task_receiver by replacing the
984        // current new_task_sender with a random one...
985        self.new_task_sender = crossbeam_channel::unbounded().0;
986
987        self.ensure_join_threads(true);
988        assert_matches!(self.session_result_with_timings, Some((Ok(_), _)));
989    }
990}
991
992impl<S, TH> PooledSchedulerInner<S, TH>
993where
994    S: SpawnableScheduler<TH>,
995    TH: TaskHandler,
996{
997    fn is_aborted(&self) -> bool {
998        // Schedulers can be regarded as being _trashed_ (thereby will be cleaned up later), if
999        // threads are joined. Remember that unified scheduler _doesn't normally join threads_ even
1000        // across different sessions (i.e. different banks) to avoid thread recreation overhead.
1001        //
1002        // These unusual thread joining happens after the blocked thread (= the replay stage)'s
1003        // detection of aborted scheduler thread, which can be interpreted as an immediate signal
1004        // about the existence of the transaction error.
1005        //
1006        // Note that this detection is done internally every time scheduler operations are run
1007        // (send_task() and end_session(); or schedule_execution() and wait_for_termination() in
1008        // terms of InstalledScheduler). So, it's ensured that the detection is done at least once
1009        // for any scheduler which is taken out of the pool.
1010        //
1011        // Thus, any transaction errors are always handled without loss of information and
1012        // the aborted scheduler itself will always be handled as _trashed_ before returning the
1013        // scheduler to the pool, considering is_aborted() is checked via is_trashed() immediately
1014        // before that.
1015        self.thread_manager.are_threads_joined()
1016    }
1017
1018    fn is_overgrown(&self) -> bool {
1019        self.usage_queue_loader.count() > self.thread_manager.pool.max_usage_queue_count
1020    }
1021}
1022
1023// This type manages the OS threads for scheduling and executing transactions. The term
1024// `session` is consistently used to mean a group of Tasks scoped under a single SchedulingContext.
1025// This is equivalent to a particular bank for block verification. However, new terms is introduced
1026// here to mean some continuous time over multiple continuous banks/slots for the block production,
1027// which is planned to be implemented in the future.
1028#[derive(Debug)]
1029struct ThreadManager<S: SpawnableScheduler<TH>, TH: TaskHandler> {
1030    scheduler_id: SchedulerId,
1031    pool: Arc<SchedulerPool<S, TH>>,
1032    new_task_sender: Sender<NewTaskPayload>,
1033    new_task_receiver: Option<Receiver<NewTaskPayload>>,
1034    session_result_sender: Sender<ResultWithTimings>,
1035    session_result_receiver: Receiver<ResultWithTimings>,
1036    session_result_with_timings: Option<ResultWithTimings>,
1037    scheduler_thread: Option<JoinHandle<()>>,
1038    handler_threads: Vec<JoinHandle<()>>,
1039}
1040
1041struct HandlerPanicked;
1042type HandlerResult = std::result::Result<Box<ExecutedTask>, HandlerPanicked>;
1043
1044impl<S: SpawnableScheduler<TH>, TH: TaskHandler> ThreadManager<S, TH> {
1045    fn new(pool: Arc<SchedulerPool<S, TH>>) -> Self {
1046        let (new_task_sender, new_task_receiver) = crossbeam_channel::unbounded();
1047        let (session_result_sender, session_result_receiver) = crossbeam_channel::unbounded();
1048
1049        Self {
1050            scheduler_id: pool.new_scheduler_id(),
1051            pool,
1052            new_task_sender,
1053            new_task_receiver: Some(new_task_receiver),
1054            session_result_sender,
1055            session_result_receiver,
1056            session_result_with_timings: None,
1057            scheduler_thread: None,
1058            handler_threads: vec![],
1059        }
1060    }
1061
1062    fn execute_task_with_handler(
1063        scheduling_context: &SchedulingContext,
1064        executed_task: &mut Box<ExecutedTask>,
1065        handler_context: &HandlerContext,
1066    ) {
1067        debug!("handling task at {:?}", thread::current());
1068        TH::handle(
1069            &mut executed_task.result_with_timings.0,
1070            &mut executed_task.result_with_timings.1,
1071            scheduling_context,
1072            &executed_task.task,
1073            handler_context,
1074        );
1075    }
1076
1077    #[must_use]
1078    fn accumulate_result_with_timings(
1079        (result, timings): &mut ResultWithTimings,
1080        executed_task: HandlerResult,
1081    ) -> Option<Box<ExecutedTask>> {
1082        let Ok(executed_task) = executed_task else {
1083            return None;
1084        };
1085        timings.accumulate(&executed_task.result_with_timings.1);
1086        match executed_task.result_with_timings.0 {
1087            Ok(()) => Some(executed_task),
1088            Err(error) => {
1089                error!("error is detected while accumulating....: {error:?}");
1090                *result = Err(error);
1091                None
1092            }
1093        }
1094    }
1095
1096    fn take_session_result_with_timings(&mut self) -> ResultWithTimings {
1097        self.session_result_with_timings.take().unwrap()
1098    }
1099
1100    fn put_session_result_with_timings(&mut self, result_with_timings: ResultWithTimings) {
1101        assert_matches!(
1102            self.session_result_with_timings
1103                .replace(result_with_timings),
1104            None
1105        );
1106    }
1107
1108    // This method must take same set of session-related arguments as start_session() to avoid
1109    // unneeded channel operations to minimize overhead. Starting threads incurs a very high cost
1110    // already... Also, pre-creating threads isn't desirable as well to avoid `Option`-ed types
1111    // for type safety.
1112    fn start_threads(
1113        &mut self,
1114        context: SchedulingContext,
1115        mut result_with_timings: ResultWithTimings,
1116        handler_context: HandlerContext,
1117    ) {
1118        // Firstly, setup bi-directional messaging between the scheduler and handlers to pass
1119        // around tasks, by creating 2 channels (one for to-be-handled tasks from the scheduler to
1120        // the handlers and the other for finished tasks from the handlers to the scheduler).
1121        // Furthermore, this pair of channels is duplicated to work as a primitive 2-level priority
1122        // queue, totalling 4 channels. Note that the two scheduler-to-handler channels are managed
1123        // behind chained_channel to avoid race conditions relating to contexts.
1124        //
1125        // This quasi-priority-queue arrangement is desired as an optimization to prioritize
1126        // blocked tasks.
1127        //
1128        // As a quick background, SchedulingStateMachine doesn't throttle runnable tasks at all.
1129        // Thus, it's likely for to-be-handled tasks to be stalled for extended duration due to
1130        // excessive buffering (commonly known as buffer bloat). Normally, this buffering isn't
1131        // problematic and actually intentional to fully saturate all the handler threads.
1132        //
1133        // However, there's one caveat: task dependencies. It can be hinted with tasks being
1134        // blocked, that there could be more similarly-blocked tasks in the future. Empirically,
1135        // clearing these linearized long runs of blocking tasks out of the buffer is delaying bank
1136        // freezing while only using 1 handler thread or two near the end of slot, deteriorating
1137        // the overall concurrency.
1138        //
1139        // To alleviate the situation, blocked tasks are exchanged via independent communication
1140        // pathway as a heuristic for expedite processing. Without prioritization of these tasks,
1141        // progression of clearing these runs would be severely hampered due to interleaved
1142        // not-blocked tasks (called _idle_ here; typically, voting transactions) in the single
1143        // buffer.
1144        //
1145        // Concurrent priority queue isn't used to avoid penalized throughput due to higher
1146        // overhead than crossbeam channel, even considering the doubled processing of the
1147        // crossbeam channel. Fortunately, just 2-level prioritization is enough. Also, sticking to
1148        // crossbeam was convenient and there was no popular and promising crate for concurrent
1149        // priority queue as of writing.
1150        //
1151        // It's generally harmless for the blocked task buffer to be flooded, stalling the idle
1152        // tasks completely. Firstly, it's unlikely without malice, considering all blocked tasks
1153        // must have independently been blocked for each isolated linearized runs. That's because
1154        // all to-be-handled tasks of the blocked and idle buffers must not be conflicting with
1155        // each other by definition. Furthermore, handler threads would still be saturated to
1156        // maximum even under such a block-verification situation, meaning no remotely-controlled
1157        // performance degradation.
1158        //
1159        // Overall, while this is merely a heuristic, it's effective and adaptive while not
1160        // vulnerable, merely reusing existing information without any additional runtime cost.
1161        //
1162        // One known caveat, though, is that this heuristic is employed under a sub-optimal
1163        // setting, considering scheduling is done in real-time. Namely, prioritization enforcement
1164        // isn't immediate, in a sense that the first task of a long run is buried in the middle of
1165        // a large idle task buffer. Prioritization of such a run will be realized only after the
1166        // first task is handled with the priority of an idle task. To overcome this, some kind of
1167        // re-prioritization or look-ahead scheduling mechanism would be needed. However, both
1168        // isn't implemented. The former is due to complex implementation and the later is due to
1169        // delayed (NOT real-time) processing, which is against the unified scheduler design goal.
1170        //
1171        // Alternatively, more faithful prioritization can be realized by checking blocking
1172        // statuses of all addresses immediately before sending to the handlers. This would prevent
1173        // false negatives of the heuristics approach (i.e. the last task of a run doesn't need to
1174        // be handled with the higher priority). Note that this is the only improvement, compared
1175        // to the heuristics. That's because this underlying information asymmetry between the 2
1176        // approaches doesn't exist for all other cases, assuming no look-ahead: idle tasks are
1177        // always unblocked by definition, and other blocked tasks should always be calculated as
1178        // blocked by the very existence of the last blocked task.
1179        //
1180        // The faithful approach incurs a considerable overhead: O(N), where N is the number of
1181        // locked addresses in a task, adding to the current bare-minimum complexity of O(2*N) for
1182        // both scheduling and descheduling. This means 1.5x increase. Furthermore, this doesn't
1183        // nicely work in practice with a real-time streamed scheduler. That's because these
1184        // linearized runs could be intermittent in the view with little or no look-back, albeit
1185        // actually forming a far more longer runs in longer time span. These access patterns are
1186        // very common, considering existence of well-known hot accounts.
1187        //
1188        // Thus, intentionally allowing these false-positives by the heuristic approach is actually
1189        // helping to extend the logical prioritization session for the invisible longer runs, as
1190        // long as the last task of the current run is being handled by the handlers, hoping yet
1191        // another blocking new task is arriving to finalize the tentatively extended
1192        // prioritization further. Consequently, this also contributes to alleviate the known
1193        // heuristic's caveat for the first task of linearized runs, which is described above.
1194        let (mut runnable_task_sender, runnable_task_receiver) =
1195            chained_channel::unbounded::<Task, SchedulingContext>(context);
1196        // Create two handler-to-scheduler channels to prioritize the finishing of blocked tasks,
1197        // because it is more likely that a blocked task will have more blocked tasks behind it,
1198        // which should be scheduled while minimizing the delay to clear buffered linearized runs
1199        // as fast as possible.
1200        let (finished_blocked_task_sender, finished_blocked_task_receiver) =
1201            crossbeam_channel::unbounded::<HandlerResult>();
1202        let (finished_idle_task_sender, finished_idle_task_receiver) =
1203            crossbeam_channel::unbounded::<HandlerResult>();
1204
1205        assert_matches!(self.session_result_with_timings, None);
1206
1207        // High-level flow of new tasks:
1208        // 1. the replay stage thread send a new task.
1209        // 2. the scheduler thread accepts the task.
1210        // 3. the scheduler thread dispatches the task after proper locking.
1211        // 4. the handler thread processes the dispatched task.
1212        // 5. the handler thread reply back to the scheduler thread as an executed task.
1213        // 6. the scheduler thread post-processes the executed task.
1214        let scheduler_main_loop = {
1215            let handler_count = self.pool.handler_count;
1216            let session_result_sender = self.session_result_sender.clone();
1217            // Taking new_task_receiver here is important to ensure there's a single receiver. In
1218            // this way, the replay stage will get .send() failures reliably, after this scheduler
1219            // thread died along with the single receiver.
1220            let new_task_receiver = self
1221                .new_task_receiver
1222                .take()
1223                .expect("no 2nd start_threads()");
1224
1225            let mut session_ending = false;
1226
1227            // Now, this is the main loop for the scheduler thread, which is a special beast.
1228            //
1229            // That's because it could be the most notable bottleneck of throughput in the future
1230            // when there are ~100 handler threads. Unified scheduler's overall throughput is
1231            // largely dependant on its ultra-low latency characteristic, which is the most
1232            // important design goal of the scheduler in order to reduce the transaction
1233            // confirmation latency for end users.
1234            //
1235            // Firstly, the scheduler thread must handle incoming messages from thread(s) owned by
1236            // the replay stage or the banking stage. It also must handle incoming messages from
1237            // the multi-threaded handlers. This heavily-multi-threaded whole processing load must
1238            // be coped just with the single-threaded scheduler, to attain ideal cpu cache
1239            // friendliness and main memory bandwidth saturation with its shared-nothing
1240            // single-threaded account locking implementation. In other words, the per-task
1241            // processing efficiency of the main loop codifies the upper bound of horizontal
1242            // scalability of the unified scheduler.
1243            //
1244            // Moreover, the scheduler is designed to handle tasks without batching at all in the
1245            // pursuit of saturating all of the handler threads with maximally-fine-grained
1246            // concurrency density for throughput as the second design goal. This design goal
1247            // relies on the assumption that there's no considerable penalty arising from the
1248            // unbatched manner of processing.
1249            //
1250            // Note that this assumption isn't true as of writing. The current code path
1251            // underneath execute_batch() isn't optimized for unified scheduler's load pattern (ie.
1252            // batches just with a single transaction) at all. This will be addressed in the
1253            // future.
1254            //
1255            // These two key elements of the design philosophy lead to the rather unforgiving
1256            // implementation burden: Degraded performance would acutely manifest from an even tiny
1257            // amount of individual cpu-bound processing delay in the scheduler thread, like when
1258            // dispatching the next conflicting task after receiving the previous finished one from
1259            // the handler.
1260            //
1261            // Thus, it's fatal for unified scheduler's advertised superiority to squeeze every cpu
1262            // cycles out of the scheduler thread. Thus, any kinds of unessential overhead sources
1263            // like syscalls, VDSO, and even memory (de)allocation should be avoided at all costs
1264            // by design or by means of offloading at the last resort.
1265            move || {
1266                let (do_now, dont_now) = (&disconnected::<()>(), &never::<()>());
1267                let dummy_receiver = |trigger| {
1268                    if trigger {
1269                        do_now
1270                    } else {
1271                        dont_now
1272                    }
1273                };
1274
1275                let mut state_machine = unsafe {
1276                    SchedulingStateMachine::exclusively_initialize_current_thread_for_scheduling()
1277                };
1278
1279                // The following loop maintains and updates ResultWithTimings as its
1280                // externally-provided mutable state for each session in this way:
1281                //
1282                // 1. Initial result_with_timing is propagated implicitly by the moved variable.
1283                // 2. Subsequent result_with_timings are propagated explicitly from
1284                //    the new_task_receiver.recv() invocation located at the end of loop.
1285                'nonaborted_main_loop: loop {
1286                    let mut is_finished = false;
1287                    while !is_finished {
1288                        // ALL recv selectors are eager-evaluated ALWAYS by current crossbeam impl,
1289                        // which isn't great and is inconsistent with `if`s in the Rust's match
1290                        // arm. So, eagerly binding the result to a variable unconditionally here
1291                        // makes no perf. difference...
1292                        let dummy_unblocked_task_receiver =
1293                            dummy_receiver(state_machine.has_unblocked_task());
1294
1295                        // There's something special called dummy_unblocked_task_receiver here.
1296                        // This odd pattern was needed to react to newly unblocked tasks from
1297                        // _not-crossbeam-channel_ event sources, precisely at the specified
1298                        // precedence among other selectors, while delegating the control flow to
1299                        // select_biased!.
1300                        //
1301                        // In this way, hot looping is avoided and overall control flow is much
1302                        // consistent. Note that unified scheduler will go
1303                        // into busy looping to seek lowest latency eventually. However, not now,
1304                        // to measure _actual_ cpu usage easily with the select approach.
1305                        select_biased! {
1306                            recv(finished_blocked_task_receiver) -> executed_task => {
1307                                let Some(executed_task) = Self::accumulate_result_with_timings(
1308                                    &mut result_with_timings,
1309                                    executed_task.expect("alive handler"),
1310                                ) else {
1311                                    break 'nonaborted_main_loop;
1312                                };
1313                                state_machine.deschedule_task(&executed_task.task);
1314                            },
1315                            recv(dummy_unblocked_task_receiver) -> dummy => {
1316                                assert_matches!(dummy, Err(RecvError));
1317
1318                                let task = state_machine
1319                                    .schedule_next_unblocked_task()
1320                                    .expect("unblocked task");
1321                                runnable_task_sender.send_payload(task).unwrap();
1322                            },
1323                            recv(new_task_receiver) -> message => {
1324                                assert!(!session_ending);
1325
1326                                match message {
1327                                    Ok(NewTaskPayload::Payload(task)) => {
1328                                        sleepless_testing::at(CheckPoint::NewTask(task.task_index()));
1329                                        if let Some(task) = state_machine.schedule_task(task) {
1330                                            runnable_task_sender.send_aux_payload(task).unwrap();
1331                                        }
1332                                    }
1333                                    Ok(NewTaskPayload::CloseSubchannel) => {
1334                                        session_ending = true;
1335                                    }
1336                                    Ok(NewTaskPayload::OpenSubchannel(_context_and_result_with_timings)) =>
1337                                        unreachable!(),
1338                                    Err(RecvError) => {
1339                                        // Mostly likely is that this scheduler is dropped for pruned blocks of
1340                                        // abandoned forks...
1341                                        // This short-circuiting is tested with test_scheduler_drop_short_circuiting.
1342                                        break 'nonaborted_main_loop;
1343                                    }
1344                                }
1345                            },
1346                            recv(finished_idle_task_receiver) -> executed_task => {
1347                                let Some(executed_task) = Self::accumulate_result_with_timings(
1348                                    &mut result_with_timings,
1349                                    executed_task.expect("alive handler"),
1350                                ) else {
1351                                    break 'nonaborted_main_loop;
1352                                };
1353                                state_machine.deschedule_task(&executed_task.task);
1354                            },
1355                        };
1356
1357                        is_finished = session_ending && state_machine.has_no_active_task();
1358                    }
1359
1360                    // Finalize the current session after asserting it's explicitly requested so.
1361                    assert!(session_ending);
1362                    // Send result first because this is blocking the replay code-path.
1363                    session_result_sender
1364                        .send(result_with_timings)
1365                        .expect("always outlived receiver");
1366                    state_machine.reinitialize();
1367                    session_ending = false;
1368
1369                    {
1370                        // Prepare for the new session.
1371                        match new_task_receiver.recv() {
1372                            Ok(NewTaskPayload::OpenSubchannel(context_and_result_with_timings)) => {
1373                                let (new_context, new_result_with_timings) =
1374                                    *context_and_result_with_timings;
1375                                // We just received subsequent (= not initial) session and about to
1376                                // enter into the preceding `while(!is_finished) {...}` loop again.
1377                                // Before that, propagate new SchedulingContext to handler threads
1378                                runnable_task_sender
1379                                    .send_chained_channel(&new_context, handler_count)
1380                                    .unwrap();
1381                                result_with_timings = new_result_with_timings;
1382                            }
1383                            Err(_) => {
1384                                // This unusual condition must be triggered by ThreadManager::drop().
1385                                // Initialize result_with_timings with a harmless value...
1386                                result_with_timings = initialized_result_with_timings();
1387                                break 'nonaborted_main_loop;
1388                            }
1389                            Ok(_) => unreachable!(),
1390                        }
1391                    }
1392                }
1393
1394                // There are several code-path reaching here out of the preceding unconditional
1395                // `loop { ... }` by the use of `break 'nonaborted_main_loop;`. This scheduler
1396                // thread will now initiate the termination process, indicating an abnormal abortion,
1397                // in order to be handled gracefully by other threads.
1398
1399                // Firstly, send result_with_timings as-is, because it's expected for us to put the
1400                // last result_with_timings into the channel without exception. Usually,
1401                // result_with_timings will contain the Err variant at this point, indicating the
1402                // occurrence of transaction error.
1403                session_result_sender
1404                    .send(result_with_timings)
1405                    .expect("always outlived receiver");
1406
1407                // Next, drop `new_task_receiver`. After that, the paired singleton
1408                // `new_task_sender` will start to error when called by external threads, resulting
1409                // in propagation of thread abortion to the external threads.
1410                drop(new_task_receiver);
1411
1412                // We will now exit this thread finally... Good bye.
1413                sleepless_testing::at(CheckPoint::SchedulerThreadAborted);
1414            }
1415        };
1416
1417        let handler_main_loop = || {
1418            let mut handler_context = handler_context.clone();
1419            let mut runnable_task_receiver = runnable_task_receiver.clone();
1420            let finished_blocked_task_sender = finished_blocked_task_sender.clone();
1421            let finished_idle_task_sender = finished_idle_task_sender.clone();
1422
1423            // The following loop maintains and updates SchedulingContext as its
1424            // externally-provided state for each session in this way:
1425            //
1426            // 1. Initial context is propagated implicitly by the moved runnable_task_receiver,
1427            //    which is clone()-d just above for this particular thread.
1428            // 2. Subsequent contexts are propagated explicitly inside `.after_select()` as part of
1429            //    `select_biased!`, which are sent from `.send_chained_channel()` in the scheduler
1430            //    thread for all-but-initial sessions.
1431            move || {
1432                loop {
1433                    let (task, sender) = select_biased! {
1434                        recv(runnable_task_receiver.for_select()) -> message => {
1435                            let Ok(message) = message else {
1436                                break;
1437                            };
1438                            if let Some(task) = runnable_task_receiver.after_select(message) {
1439                                (task, &finished_blocked_task_sender)
1440                            } else {
1441                                continue;
1442                            }
1443                        },
1444                        recv(runnable_task_receiver.aux_for_select()) -> task => {
1445                            if let Ok(task) = task {
1446                                (task, &finished_idle_task_sender)
1447                            } else {
1448                                runnable_task_receiver.never_receive_from_aux();
1449                                continue;
1450                            }
1451                        },
1452                        recv(handler_context.banking_packet_receiver) -> banking_packet => {
1453                            // See clone_solana_core::banking_stage::unified_scheduler module doc as to
1454                            // justification of this additional work in the handler thread.
1455                            let Ok(banking_packet) = banking_packet else {
1456                                info!("disconnected banking_packet_receiver");
1457                                break;
1458                            };
1459                            (handler_context.banking_packet_handler)(
1460                                handler_context.banking_stage_helper.as_ref().unwrap(),
1461                                banking_packet
1462                            );
1463                            continue;
1464                        },
1465                    };
1466                    defer! {
1467                        if !thread::panicking() {
1468                            return;
1469                        }
1470
1471                        // The scheduler thread can't detect panics in handler threads with
1472                        // disconnected channel errors, unless all of them has died. So, send an
1473                        // explicit Err promptly.
1474                        let current_thread = thread::current();
1475                        error!("handler thread is panicking: {:?}", current_thread);
1476                        if sender.send(Err(HandlerPanicked)).is_ok() {
1477                            info!("notified a panic from {:?}", current_thread);
1478                        } else {
1479                            // It seems that the scheduler thread has been aborted already...
1480                            warn!("failed to notify a panic from {:?}", current_thread);
1481                        }
1482                    }
1483                    let mut task = ExecutedTask::new_boxed(task);
1484                    Self::execute_task_with_handler(
1485                        runnable_task_receiver.context(),
1486                        &mut task,
1487                        &handler_context,
1488                    );
1489                    if sender.send(Ok(task)).is_err() {
1490                        warn!("handler_thread: scheduler thread aborted...");
1491                        break;
1492                    }
1493                }
1494            }
1495        };
1496
1497        self.scheduler_thread = Some(
1498            thread::Builder::new()
1499                .name("solScheduler".to_owned())
1500                .spawn_tracked(scheduler_main_loop)
1501                .unwrap(),
1502        );
1503
1504        self.handler_threads = (0..self.pool.handler_count)
1505            .map({
1506                |thx| {
1507                    thread::Builder::new()
1508                        .name(format!("solScHandler{:02}", thx))
1509                        .spawn_tracked(handler_main_loop())
1510                        .unwrap()
1511                }
1512            })
1513            .collect();
1514    }
1515
1516    fn send_task(&self, task: Task) -> ScheduleResult {
1517        debug!("send_task()");
1518        self.new_task_sender
1519            .send(NewTaskPayload::Payload(task))
1520            .map_err(|_| SchedulerAborted)
1521    }
1522
1523    fn ensure_join_threads(&mut self, should_receive_session_result: bool) {
1524        trace!("ensure_join_threads() is called");
1525
1526        fn join_with_panic_message(join_handle: JoinHandle<()>) -> thread::Result<()> {
1527            let thread = join_handle.thread().clone();
1528            join_handle.join().inspect_err(|e| {
1529                // Always needs to try both types for .downcast_ref(), according to
1530                // https://doc.rust-lang.org/1.78.0/std/macro.panic.html:
1531                //   a panic can be accessed as a &dyn Any + Send, which contains either a &str or
1532                //   String for regular panic!() invocations. (Whether a particular invocation
1533                //   contains the payload at type &str or String is unspecified and can change.)
1534                let panic_message = match (e.downcast_ref::<&str>(), e.downcast_ref::<String>()) {
1535                    (Some(&s), _) => s,
1536                    (_, Some(s)) => s,
1537                    (None, None) => "<No panic info>",
1538                };
1539                panic!("{} (From: {:?})", panic_message, thread);
1540            })
1541        }
1542
1543        if let Some(scheduler_thread) = self.scheduler_thread.take() {
1544            for thread in self.handler_threads.drain(..) {
1545                debug!("joining...: {:?}", thread);
1546                () = join_with_panic_message(thread).unwrap();
1547            }
1548            () = join_with_panic_message(scheduler_thread).unwrap();
1549
1550            if should_receive_session_result {
1551                let result_with_timings = self.session_result_receiver.recv().unwrap();
1552                debug!("ensure_join_threads(): err: {:?}", result_with_timings.0);
1553                self.put_session_result_with_timings(result_with_timings);
1554            }
1555        } else {
1556            warn!("ensure_join_threads(): skipping; already joined...");
1557        };
1558    }
1559
1560    fn ensure_join_threads_after_abort(
1561        &mut self,
1562        should_receive_aborted_session_result: bool,
1563    ) -> TransactionError {
1564        self.ensure_join_threads(should_receive_aborted_session_result);
1565        self.session_result_with_timings
1566            .as_mut()
1567            .unwrap()
1568            .0
1569            .clone()
1570            .unwrap_err()
1571    }
1572
1573    fn are_threads_joined(&self) -> bool {
1574        if self.scheduler_thread.is_none() {
1575            // Emptying handler_threads must be an atomic operation with scheduler_thread being
1576            // taken.
1577            assert!(self.handler_threads.is_empty());
1578            true
1579        } else {
1580            false
1581        }
1582    }
1583
1584    fn end_session(&mut self) {
1585        if self.are_threads_joined() {
1586            assert!(self.session_result_with_timings.is_some());
1587            debug!("end_session(): skipping; already joined the aborted threads..");
1588            return;
1589        } else if self.session_result_with_timings.is_some() {
1590            debug!("end_session(): skipping; already result resides within thread manager..");
1591            return;
1592        }
1593        debug!(
1594            "end_session(): will end session at {:?}...",
1595            thread::current(),
1596        );
1597
1598        let mut abort_detected = self
1599            .new_task_sender
1600            .send(NewTaskPayload::CloseSubchannel)
1601            .is_err();
1602
1603        if abort_detected {
1604            self.ensure_join_threads_after_abort(true);
1605            return;
1606        }
1607
1608        // Even if abort is detected, it's guaranteed that the scheduler thread puts the last
1609        // message into the session_result_sender before terminating.
1610        let result_with_timings = self.session_result_receiver.recv().unwrap();
1611        abort_detected = result_with_timings.0.is_err();
1612        self.put_session_result_with_timings(result_with_timings);
1613        if abort_detected {
1614            self.ensure_join_threads_after_abort(false);
1615        }
1616        debug!("end_session(): ended session at {:?}...", thread::current());
1617    }
1618
1619    fn start_session(
1620        &mut self,
1621        context: SchedulingContext,
1622        result_with_timings: ResultWithTimings,
1623    ) {
1624        assert!(!self.are_threads_joined());
1625        assert_matches!(self.session_result_with_timings, None);
1626        self.new_task_sender
1627            .send(NewTaskPayload::OpenSubchannel(Box::new((
1628                context,
1629                result_with_timings,
1630            ))))
1631            .expect("no new session after aborted");
1632    }
1633}
1634
1635pub trait SchedulerInner {
1636    fn id(&self) -> SchedulerId;
1637    fn is_trashed(&self) -> bool;
1638}
1639
1640pub trait SpawnableScheduler<TH: TaskHandler>: InstalledScheduler {
1641    type Inner: SchedulerInner + Debug + Send + Sync;
1642
1643    fn into_inner(self) -> (ResultWithTimings, Self::Inner);
1644
1645    fn from_inner(
1646        inner: Self::Inner,
1647        context: SchedulingContext,
1648        result_with_timings: ResultWithTimings,
1649    ) -> Self;
1650
1651    fn spawn(
1652        pool: Arc<SchedulerPool<Self, TH>>,
1653        context: SchedulingContext,
1654        result_with_timings: ResultWithTimings,
1655    ) -> Self
1656    where
1657        Self: Sized;
1658}
1659
1660impl<TH: TaskHandler> SpawnableScheduler<TH> for PooledScheduler<TH> {
1661    type Inner = PooledSchedulerInner<Self, TH>;
1662
1663    fn into_inner(mut self) -> (ResultWithTimings, Self::Inner) {
1664        let result_with_timings = {
1665            let manager = &mut self.inner.thread_manager;
1666            manager.end_session();
1667            manager.take_session_result_with_timings()
1668        };
1669        (result_with_timings, self.inner)
1670    }
1671
1672    fn from_inner(
1673        mut inner: Self::Inner,
1674        context: SchedulingContext,
1675        result_with_timings: ResultWithTimings,
1676    ) -> Self {
1677        inner
1678            .thread_manager
1679            .start_session(context.clone(), result_with_timings);
1680        Self { inner, context }
1681    }
1682
1683    fn spawn(
1684        pool: Arc<SchedulerPool<Self, TH>>,
1685        context: SchedulingContext,
1686        result_with_timings: ResultWithTimings,
1687    ) -> Self {
1688        let mut inner = Self::Inner {
1689            thread_manager: ThreadManager::new(pool.clone()),
1690            usage_queue_loader: UsageQueueLoader::default(),
1691        };
1692        inner.thread_manager.start_threads(
1693            context.clone(),
1694            result_with_timings,
1695            pool.create_handler_context(context.mode(), &inner.thread_manager.new_task_sender),
1696        );
1697        Self { inner, context }
1698    }
1699}
1700
1701impl<TH: TaskHandler> InstalledScheduler for PooledScheduler<TH> {
1702    fn id(&self) -> SchedulerId {
1703        self.inner.id()
1704    }
1705
1706    fn context(&self) -> &SchedulingContext {
1707        &self.context
1708    }
1709
1710    fn schedule_execution(
1711        &self,
1712        transaction: RuntimeTransaction<SanitizedTransaction>,
1713        index: usize,
1714    ) -> ScheduleResult {
1715        let task = SchedulingStateMachine::create_task(transaction, index, &mut |pubkey| {
1716            self.inner.usage_queue_loader.load(pubkey)
1717        });
1718        self.inner.thread_manager.send_task(task)
1719    }
1720
1721    fn recover_error_after_abort(&mut self) -> TransactionError {
1722        self.inner
1723            .thread_manager
1724            .ensure_join_threads_after_abort(true)
1725    }
1726
1727    fn wait_for_termination(
1728        self: Box<Self>,
1729        _is_dropped: bool,
1730    ) -> (ResultWithTimings, UninstalledSchedulerBox) {
1731        let (result_with_timings, uninstalled_scheduler) = self.into_inner();
1732        (result_with_timings, Box::new(uninstalled_scheduler))
1733    }
1734
1735    fn pause_for_recent_blockhash(&mut self) {
1736        self.inner.thread_manager.end_session();
1737    }
1738}
1739
1740impl<S, TH> SchedulerInner for PooledSchedulerInner<S, TH>
1741where
1742    S: SpawnableScheduler<TH>,
1743    TH: TaskHandler,
1744{
1745    fn id(&self) -> SchedulerId {
1746        self.thread_manager.scheduler_id
1747    }
1748
1749    fn is_trashed(&self) -> bool {
1750        self.is_aborted() || self.is_overgrown()
1751    }
1752}
1753
1754impl<S, TH> UninstalledScheduler for PooledSchedulerInner<S, TH>
1755where
1756    S: SpawnableScheduler<TH, Inner = Self>,
1757    TH: TaskHandler,
1758{
1759    fn return_to_pool(self: Box<Self>) {
1760        self.thread_manager.pool.clone().return_scheduler(*self);
1761    }
1762}
1763
1764#[cfg(test)]
1765mod tests {
1766    use {
1767        super::*,
1768        crate::sleepless_testing,
1769        assert_matches::assert_matches,
1770        clone_solana_clock::{Slot, MAX_PROCESSING_AGE},
1771        clone_solana_hash::Hash,
1772        clone_solana_keypair::Keypair,
1773        clone_solana_ledger::{
1774            blockstore::Blockstore,
1775            blockstore_processor::{TransactionStatusBatch, TransactionStatusMessage},
1776            create_new_tmp_ledger_auto_delete,
1777            leader_schedule_cache::LeaderScheduleCache,
1778        },
1779        clone_solana_poh::poh_recorder::create_test_recorder_with_index_tracking,
1780        clone_solana_pubkey::Pubkey,
1781        clone_solana_runtime::{
1782            bank::Bank,
1783            bank_forks::BankForks,
1784            genesis_utils::{create_genesis_config, GenesisConfigInfo},
1785            installed_scheduler_pool::{BankWithScheduler, SchedulingContext},
1786            prioritization_fee_cache::PrioritizationFeeCache,
1787        },
1788        clone_solana_system_transaction as system_transaction,
1789        clone_solana_timings::ExecuteTimingType,
1790        clone_solana_transaction::sanitized::SanitizedTransaction,
1791        clone_solana_transaction_error::TransactionError,
1792        std::{
1793            sync::{atomic::Ordering, Arc, RwLock},
1794            thread::JoinHandle,
1795        },
1796        test_case::test_matrix,
1797    };
1798
1799    #[derive(Debug)]
1800    enum TestCheckPoint {
1801        BeforeNewTask,
1802        AfterTaskHandled,
1803        AfterSchedulerThreadAborted,
1804        BeforeIdleSchedulerCleaned,
1805        AfterIdleSchedulerCleaned,
1806        BeforeTrashedSchedulerCleaned,
1807        AfterTrashedSchedulerCleaned,
1808        BeforeTimeoutListenerTriggered,
1809        AfterTimeoutListenerTriggered,
1810        BeforeThreadManagerDrop,
1811        BeforeEndSession,
1812    }
1813
1814    #[test]
1815    fn test_scheduler_pool_new() {
1816        clone_solana_logger::setup();
1817
1818        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
1819        let pool =
1820            DefaultSchedulerPool::new_dyn(None, None, None, None, ignored_prioritization_fee_cache);
1821
1822        // this indirectly proves that there should be circular link because there's only one Arc
1823        // at this moment now
1824        // the 2 weaks are for the weak_self field and the pool cleaner thread.
1825        assert_eq!((Arc::strong_count(&pool), Arc::weak_count(&pool)), (1, 2));
1826        let debug = format!("{pool:#?}");
1827        assert!(!debug.is_empty());
1828    }
1829
1830    #[test]
1831    fn test_scheduler_spawn() {
1832        clone_solana_logger::setup();
1833
1834        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
1835        let pool =
1836            DefaultSchedulerPool::new_dyn(None, None, None, None, ignored_prioritization_fee_cache);
1837        let bank = Arc::new(Bank::default_for_tests());
1838        let context = SchedulingContext::new(bank);
1839        let scheduler = pool.take_scheduler(context);
1840
1841        let debug = format!("{scheduler:#?}");
1842        assert!(!debug.is_empty());
1843    }
1844
1845    const SHORTENED_POOL_CLEANER_INTERVAL: Duration = Duration::from_millis(1);
1846    const SHORTENED_MAX_POOLING_DURATION: Duration = Duration::from_millis(100);
1847
1848    #[test]
1849    fn test_scheduler_drop_idle() {
1850        clone_solana_logger::setup();
1851
1852        let _progress = sleepless_testing::setup(&[
1853            &TestCheckPoint::BeforeIdleSchedulerCleaned,
1854            &CheckPoint::IdleSchedulerCleaned(0),
1855            &CheckPoint::IdleSchedulerCleaned(1),
1856            &TestCheckPoint::AfterIdleSchedulerCleaned,
1857        ]);
1858
1859        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
1860        let pool_raw = DefaultSchedulerPool::do_new(
1861            None,
1862            None,
1863            None,
1864            None,
1865            ignored_prioritization_fee_cache,
1866            SHORTENED_POOL_CLEANER_INTERVAL,
1867            SHORTENED_MAX_POOLING_DURATION,
1868            DEFAULT_MAX_USAGE_QUEUE_COUNT,
1869            DEFAULT_TIMEOUT_DURATION,
1870        );
1871        let pool = pool_raw.clone();
1872        let bank = Arc::new(Bank::default_for_tests());
1873        let context1 = SchedulingContext::new(bank);
1874        let context2 = context1.clone();
1875
1876        let old_scheduler = pool.do_take_scheduler(context1);
1877        let new_scheduler = pool.do_take_scheduler(context2);
1878        let new_scheduler_id = new_scheduler.id();
1879        Box::new(old_scheduler.into_inner().1).return_to_pool();
1880
1881        // sleepless_testing can't be used; wait a bit here to see real progress of wall time...
1882        sleep(SHORTENED_MAX_POOLING_DURATION * 10);
1883        Box::new(new_scheduler.into_inner().1).return_to_pool();
1884
1885        // Block solScCleaner until we see returned schedlers...
1886        assert_eq!(pool_raw.scheduler_inners.lock().unwrap().len(), 2);
1887        sleepless_testing::at(TestCheckPoint::BeforeIdleSchedulerCleaned);
1888
1889        // See the old (= idle) scheduler gone only after solScCleaner did its job...
1890        sleepless_testing::at(&TestCheckPoint::AfterIdleSchedulerCleaned);
1891
1892        // The following assertion is racy.
1893        //
1894        // We need to make sure new_scheduler isn't treated as idle up to now since being returned
1895        // to the pool after sleep(SHORTENED_MAX_POOLING_DURATION * 10).
1896        // Removing only old_scheduler is the expected behavior. So, make
1897        // SHORTENED_MAX_POOLING_DURATION rather long...
1898        assert_eq!(pool_raw.scheduler_inners.lock().unwrap().len(), 1);
1899        assert_eq!(
1900            pool_raw
1901                .scheduler_inners
1902                .lock()
1903                .unwrap()
1904                .first()
1905                .as_ref()
1906                .map(|(inner, _pooled_at)| inner.id())
1907                .unwrap(),
1908            new_scheduler_id
1909        );
1910    }
1911
1912    #[test]
1913    fn test_scheduler_drop_overgrown() {
1914        clone_solana_logger::setup();
1915
1916        let _progress = sleepless_testing::setup(&[
1917            &TestCheckPoint::BeforeTrashedSchedulerCleaned,
1918            &CheckPoint::TrashedSchedulerCleaned(0),
1919            &CheckPoint::TrashedSchedulerCleaned(1),
1920            &TestCheckPoint::AfterTrashedSchedulerCleaned,
1921        ]);
1922
1923        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
1924        const REDUCED_MAX_USAGE_QUEUE_COUNT: usize = 1;
1925        let pool_raw = DefaultSchedulerPool::do_new(
1926            None,
1927            None,
1928            None,
1929            None,
1930            ignored_prioritization_fee_cache,
1931            SHORTENED_POOL_CLEANER_INTERVAL,
1932            DEFAULT_MAX_POOLING_DURATION,
1933            REDUCED_MAX_USAGE_QUEUE_COUNT,
1934            DEFAULT_TIMEOUT_DURATION,
1935        );
1936        let pool = pool_raw.clone();
1937        let bank = Arc::new(Bank::default_for_tests());
1938        let context1 = SchedulingContext::new(bank);
1939        let context2 = context1.clone();
1940
1941        let small_scheduler = pool.do_take_scheduler(context1);
1942        let small_scheduler_id = small_scheduler.id();
1943        for _ in 0..REDUCED_MAX_USAGE_QUEUE_COUNT {
1944            small_scheduler
1945                .inner
1946                .usage_queue_loader
1947                .load(Pubkey::new_unique());
1948        }
1949        let big_scheduler = pool.do_take_scheduler(context2);
1950        for _ in 0..REDUCED_MAX_USAGE_QUEUE_COUNT + 1 {
1951            big_scheduler
1952                .inner
1953                .usage_queue_loader
1954                .load(Pubkey::new_unique());
1955        }
1956
1957        assert_eq!(pool_raw.scheduler_inners.lock().unwrap().len(), 0);
1958        assert_eq!(pool_raw.trashed_scheduler_inners.lock().unwrap().len(), 0);
1959        Box::new(small_scheduler.into_inner().1).return_to_pool();
1960        Box::new(big_scheduler.into_inner().1).return_to_pool();
1961
1962        // Block solScCleaner until we see trashed schedler...
1963        assert_eq!(pool_raw.scheduler_inners.lock().unwrap().len(), 1);
1964        assert_eq!(pool_raw.trashed_scheduler_inners.lock().unwrap().len(), 1);
1965        sleepless_testing::at(TestCheckPoint::BeforeTrashedSchedulerCleaned);
1966
1967        // See the trashed scheduler gone only after solScCleaner did its job...
1968        sleepless_testing::at(&TestCheckPoint::AfterTrashedSchedulerCleaned);
1969        assert_eq!(pool_raw.scheduler_inners.lock().unwrap().len(), 1);
1970        assert_eq!(pool_raw.trashed_scheduler_inners.lock().unwrap().len(), 0);
1971        assert_eq!(
1972            pool_raw
1973                .scheduler_inners
1974                .lock()
1975                .unwrap()
1976                .first()
1977                .as_ref()
1978                .map(|(inner, _pooled_at)| inner.id())
1979                .unwrap(),
1980            small_scheduler_id
1981        );
1982    }
1983
1984    const SHORTENED_TIMEOUT_DURATION: Duration = Duration::from_millis(1);
1985
1986    #[test]
1987    fn test_scheduler_drop_stale() {
1988        clone_solana_logger::setup();
1989
1990        let _progress = sleepless_testing::setup(&[
1991            &TestCheckPoint::BeforeTimeoutListenerTriggered,
1992            &CheckPoint::TimeoutListenerTriggered(0),
1993            &CheckPoint::TimeoutListenerTriggered(1),
1994            &TestCheckPoint::AfterTimeoutListenerTriggered,
1995            &CheckPoint::IdleSchedulerCleaned(1),
1996            &TestCheckPoint::AfterIdleSchedulerCleaned,
1997        ]);
1998
1999        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2000        let pool_raw = DefaultSchedulerPool::do_new(
2001            None,
2002            None,
2003            None,
2004            None,
2005            ignored_prioritization_fee_cache,
2006            SHORTENED_POOL_CLEANER_INTERVAL,
2007            SHORTENED_MAX_POOLING_DURATION,
2008            DEFAULT_MAX_USAGE_QUEUE_COUNT,
2009            SHORTENED_TIMEOUT_DURATION,
2010        );
2011        let pool = pool_raw.clone();
2012        let bank = Arc::new(Bank::default_for_tests());
2013        let context = SchedulingContext::new(bank.clone());
2014        let scheduler = pool.take_scheduler(context);
2015        let bank = BankWithScheduler::new(bank, Some(scheduler));
2016        pool.register_timeout_listener(bank.create_timeout_listener());
2017        assert_eq!(pool_raw.scheduler_inners.lock().unwrap().len(), 0);
2018        assert_eq!(pool_raw.trashed_scheduler_inners.lock().unwrap().len(), 0);
2019        sleepless_testing::at(TestCheckPoint::BeforeTimeoutListenerTriggered);
2020
2021        sleepless_testing::at(TestCheckPoint::AfterTimeoutListenerTriggered);
2022        assert_eq!(pool_raw.scheduler_inners.lock().unwrap().len(), 1);
2023        assert_eq!(pool_raw.trashed_scheduler_inners.lock().unwrap().len(), 0);
2024        assert_matches!(bank.wait_for_completed_scheduler(), Some((Ok(()), _)));
2025
2026        // See the stale scheduler gone only after solScCleaner did its job...
2027        sleepless_testing::at(&TestCheckPoint::AfterIdleSchedulerCleaned);
2028        assert_eq!(pool_raw.scheduler_inners.lock().unwrap().len(), 0);
2029        assert_eq!(pool_raw.trashed_scheduler_inners.lock().unwrap().len(), 0);
2030    }
2031
2032    #[test]
2033    fn test_scheduler_active_after_stale() {
2034        clone_solana_logger::setup();
2035
2036        let _progress = sleepless_testing::setup(&[
2037            &TestCheckPoint::BeforeTimeoutListenerTriggered,
2038            &CheckPoint::TimeoutListenerTriggered(0),
2039            &CheckPoint::TimeoutListenerTriggered(1),
2040            &TestCheckPoint::AfterTimeoutListenerTriggered,
2041            &TestCheckPoint::BeforeTimeoutListenerTriggered,
2042            &CheckPoint::TimeoutListenerTriggered(0),
2043            &CheckPoint::TimeoutListenerTriggered(1),
2044            &TestCheckPoint::AfterTimeoutListenerTriggered,
2045        ]);
2046
2047        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2048        let pool_raw = SchedulerPool::<PooledScheduler<ExecuteTimingCounter>, _>::do_new(
2049            None,
2050            None,
2051            None,
2052            None,
2053            ignored_prioritization_fee_cache,
2054            SHORTENED_POOL_CLEANER_INTERVAL,
2055            DEFAULT_MAX_POOLING_DURATION,
2056            DEFAULT_MAX_USAGE_QUEUE_COUNT,
2057            SHORTENED_TIMEOUT_DURATION,
2058        );
2059
2060        #[derive(Debug)]
2061        struct ExecuteTimingCounter;
2062        impl TaskHandler for ExecuteTimingCounter {
2063            fn handle(
2064                _result: &mut Result<()>,
2065                timings: &mut ExecuteTimings,
2066                _bank: &SchedulingContext,
2067                _task: &Task,
2068                _handler_context: &HandlerContext,
2069            ) {
2070                timings.metrics[ExecuteTimingType::CheckUs] += 123;
2071            }
2072        }
2073        let pool = pool_raw.clone();
2074
2075        let GenesisConfigInfo {
2076            genesis_config,
2077            mint_keypair,
2078            ..
2079        } = create_genesis_config(10_000);
2080        let bank = Bank::new_for_tests(&genesis_config);
2081        let (bank, _bank_forks) = setup_dummy_fork_graph(bank);
2082
2083        let context = SchedulingContext::new(bank.clone());
2084
2085        let scheduler = pool.take_scheduler(context);
2086        let bank = BankWithScheduler::new(bank, Some(scheduler));
2087        pool.register_timeout_listener(bank.create_timeout_listener());
2088
2089        let tx_before_stale =
2090            RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
2091                &mint_keypair,
2092                &clone_solana_pubkey::new_rand(),
2093                2,
2094                genesis_config.hash(),
2095            ));
2096        bank.schedule_transaction_executions([(tx_before_stale, 0)].into_iter())
2097            .unwrap();
2098        sleepless_testing::at(TestCheckPoint::BeforeTimeoutListenerTriggered);
2099
2100        sleepless_testing::at(TestCheckPoint::AfterTimeoutListenerTriggered);
2101        let tx_after_stale =
2102            RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
2103                &mint_keypair,
2104                &clone_solana_pubkey::new_rand(),
2105                2,
2106                genesis_config.hash(),
2107            ));
2108        bank.schedule_transaction_executions([(tx_after_stale, 1)].into_iter())
2109            .unwrap();
2110
2111        // Observe second occurrence of TimeoutListenerTriggered(1), which indicates a new timeout
2112        // lister is registered correctly again for reactivated scheduler.
2113        sleepless_testing::at(TestCheckPoint::BeforeTimeoutListenerTriggered);
2114        sleepless_testing::at(TestCheckPoint::AfterTimeoutListenerTriggered);
2115
2116        let (result, timings) = bank.wait_for_completed_scheduler().unwrap();
2117        assert_matches!(result, Ok(()));
2118        // ResultWithTimings should be carried over across active=>stale=>active transitions.
2119        assert_eq!(timings.metrics[ExecuteTimingType::CheckUs].0, 246);
2120    }
2121
2122    #[test]
2123    fn test_scheduler_pause_after_stale() {
2124        clone_solana_logger::setup();
2125
2126        let _progress = sleepless_testing::setup(&[
2127            &TestCheckPoint::BeforeTimeoutListenerTriggered,
2128            &CheckPoint::TimeoutListenerTriggered(0),
2129            &CheckPoint::TimeoutListenerTriggered(1),
2130            &TestCheckPoint::AfterTimeoutListenerTriggered,
2131        ]);
2132
2133        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2134        let pool_raw = DefaultSchedulerPool::do_new(
2135            None,
2136            None,
2137            None,
2138            None,
2139            ignored_prioritization_fee_cache,
2140            SHORTENED_POOL_CLEANER_INTERVAL,
2141            DEFAULT_MAX_POOLING_DURATION,
2142            DEFAULT_MAX_USAGE_QUEUE_COUNT,
2143            SHORTENED_TIMEOUT_DURATION,
2144        );
2145        let pool = pool_raw.clone();
2146
2147        let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(10_000);
2148        let bank = Bank::new_for_tests(&genesis_config);
2149        let (bank, _bank_forks) = setup_dummy_fork_graph(bank);
2150
2151        let context = SchedulingContext::new(bank.clone());
2152
2153        let scheduler = pool.take_scheduler(context);
2154        let bank = BankWithScheduler::new(bank, Some(scheduler));
2155        pool.register_timeout_listener(bank.create_timeout_listener());
2156
2157        sleepless_testing::at(TestCheckPoint::BeforeTimeoutListenerTriggered);
2158        sleepless_testing::at(TestCheckPoint::AfterTimeoutListenerTriggered);
2159
2160        // This calls register_recent_blockhash() internally, which in turn calls
2161        // BankWithScheduler::wait_for_paused_scheduler().
2162        bank.fill_bank_with_ticks_for_tests();
2163        let (result, _timings) = bank.wait_for_completed_scheduler().unwrap();
2164        assert_matches!(result, Ok(()));
2165    }
2166
2167    #[test]
2168    fn test_scheduler_remain_stale_after_error() {
2169        clone_solana_logger::setup();
2170
2171        let _progress = sleepless_testing::setup(&[
2172            &TestCheckPoint::BeforeTimeoutListenerTriggered,
2173            &CheckPoint::TimeoutListenerTriggered(0),
2174            &CheckPoint::SchedulerThreadAborted,
2175            &TestCheckPoint::AfterSchedulerThreadAborted,
2176            &CheckPoint::TimeoutListenerTriggered(1),
2177            &TestCheckPoint::AfterTimeoutListenerTriggered,
2178        ]);
2179
2180        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2181        let pool_raw = SchedulerPool::<PooledScheduler<FaultyHandler>, _>::do_new(
2182            None,
2183            None,
2184            None,
2185            None,
2186            ignored_prioritization_fee_cache,
2187            SHORTENED_POOL_CLEANER_INTERVAL,
2188            DEFAULT_MAX_POOLING_DURATION,
2189            DEFAULT_MAX_USAGE_QUEUE_COUNT,
2190            SHORTENED_TIMEOUT_DURATION,
2191        );
2192
2193        let pool = pool_raw.clone();
2194
2195        let GenesisConfigInfo {
2196            genesis_config,
2197            mint_keypair,
2198            ..
2199        } = create_genesis_config(10_000);
2200        let bank = Bank::new_for_tests(&genesis_config);
2201        let (bank, _bank_forks) = setup_dummy_fork_graph(bank);
2202
2203        let context = SchedulingContext::new(bank.clone());
2204
2205        let scheduler = pool.take_scheduler(context);
2206        let bank = BankWithScheduler::new(bank, Some(scheduler));
2207        pool.register_timeout_listener(bank.create_timeout_listener());
2208
2209        let tx_before_stale =
2210            RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
2211                &mint_keypair,
2212                &clone_solana_pubkey::new_rand(),
2213                2,
2214                genesis_config.hash(),
2215            ));
2216        bank.schedule_transaction_executions([(tx_before_stale, 0)].into_iter())
2217            .unwrap();
2218        sleepless_testing::at(TestCheckPoint::BeforeTimeoutListenerTriggered);
2219        sleepless_testing::at(TestCheckPoint::AfterSchedulerThreadAborted);
2220
2221        sleepless_testing::at(TestCheckPoint::AfterTimeoutListenerTriggered);
2222        let tx_after_stale =
2223            RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
2224                &mint_keypair,
2225                &clone_solana_pubkey::new_rand(),
2226                2,
2227                genesis_config.hash(),
2228            ));
2229        let result = bank.schedule_transaction_executions([(tx_after_stale, 1)].into_iter());
2230        assert_matches!(result, Err(TransactionError::AccountNotFound));
2231
2232        let (result, _timings) = bank.wait_for_completed_scheduler().unwrap();
2233        assert_matches!(result, Err(TransactionError::AccountNotFound));
2234    }
2235
2236    enum AbortCase {
2237        Unhandled,
2238        UnhandledWhilePanicking,
2239        Handled,
2240    }
2241
2242    #[derive(Debug)]
2243    struct FaultyHandler;
2244    impl TaskHandler for FaultyHandler {
2245        fn handle(
2246            result: &mut Result<()>,
2247            _timings: &mut ExecuteTimings,
2248            _bank: &SchedulingContext,
2249            _task: &Task,
2250            _handler_context: &HandlerContext,
2251        ) {
2252            *result = Err(TransactionError::AccountNotFound);
2253        }
2254    }
2255
2256    fn do_test_scheduler_drop_abort(abort_case: AbortCase) {
2257        clone_solana_logger::setup();
2258
2259        let _progress = sleepless_testing::setup(match abort_case {
2260            AbortCase::Unhandled => &[
2261                &CheckPoint::SchedulerThreadAborted,
2262                &TestCheckPoint::AfterSchedulerThreadAborted,
2263            ],
2264            _ => &[],
2265        });
2266
2267        let GenesisConfigInfo {
2268            genesis_config,
2269            mint_keypair,
2270            ..
2271        } = create_genesis_config(10_000);
2272
2273        let tx = RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
2274            &mint_keypair,
2275            &clone_solana_pubkey::new_rand(),
2276            2,
2277            genesis_config.hash(),
2278        ));
2279
2280        let bank = Bank::new_for_tests(&genesis_config);
2281        let (bank, _bank_forks) = setup_dummy_fork_graph(bank);
2282        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2283        let pool = SchedulerPool::<PooledScheduler<FaultyHandler>, _>::new(
2284            None,
2285            None,
2286            None,
2287            None,
2288            ignored_prioritization_fee_cache,
2289        );
2290        let context = SchedulingContext::new(bank.clone());
2291        let scheduler = pool.do_take_scheduler(context);
2292        scheduler.schedule_execution(tx, 0).unwrap();
2293
2294        match abort_case {
2295            AbortCase::Unhandled => {
2296                sleepless_testing::at(TestCheckPoint::AfterSchedulerThreadAborted);
2297                // Directly dropping PooledScheduler is illegal unless panicking already, especially
2298                // after being aborted. It must be converted to PooledSchedulerInner via
2299                // ::into_inner();
2300                drop::<PooledScheduler<_>>(scheduler);
2301            }
2302            AbortCase::UnhandledWhilePanicking => {
2303                // no sleepless_testing::at(); panicking special-casing isn't racy
2304                panic!("ThreadManager::drop() should be skipped...");
2305            }
2306            AbortCase::Handled => {
2307                // no sleepless_testing::at(); ::into_inner() isn't racy
2308                let ((result, _), mut scheduler_inner) = scheduler.into_inner();
2309                assert_matches!(result, Err(TransactionError::AccountNotFound));
2310
2311                // Calling ensure_join_threads() repeatedly should be safe.
2312                let dummy_flag = true; // doesn't matter because it's skipped anyway
2313                scheduler_inner
2314                    .thread_manager
2315                    .ensure_join_threads(dummy_flag);
2316
2317                drop::<PooledSchedulerInner<_, _>>(scheduler_inner);
2318            }
2319        }
2320    }
2321
2322    #[test]
2323    #[should_panic(expected = "does not match `Some((Ok(_), _))")]
2324    fn test_scheduler_drop_abort_unhandled() {
2325        do_test_scheduler_drop_abort(AbortCase::Unhandled);
2326    }
2327
2328    #[test]
2329    #[should_panic(expected = "ThreadManager::drop() should be skipped...")]
2330    fn test_scheduler_drop_abort_unhandled_while_panicking() {
2331        do_test_scheduler_drop_abort(AbortCase::UnhandledWhilePanicking);
2332    }
2333
2334    #[test]
2335    fn test_scheduler_drop_abort_handled() {
2336        do_test_scheduler_drop_abort(AbortCase::Handled);
2337    }
2338
2339    #[test]
2340    fn test_scheduler_drop_short_circuiting() {
2341        clone_solana_logger::setup();
2342
2343        let _progress = sleepless_testing::setup(&[
2344            &TestCheckPoint::BeforeThreadManagerDrop,
2345            &CheckPoint::NewTask(0),
2346            &CheckPoint::SchedulerThreadAborted,
2347            &TestCheckPoint::AfterSchedulerThreadAborted,
2348        ]);
2349
2350        static TASK_COUNT: Mutex<usize> = Mutex::new(0);
2351
2352        #[derive(Debug)]
2353        struct CountingHandler;
2354        impl TaskHandler for CountingHandler {
2355            fn handle(
2356                _result: &mut Result<()>,
2357                _timings: &mut ExecuteTimings,
2358                _bank: &SchedulingContext,
2359                _task: &Task,
2360                _handler_context: &HandlerContext,
2361            ) {
2362                *TASK_COUNT.lock().unwrap() += 1;
2363            }
2364        }
2365
2366        let GenesisConfigInfo {
2367            genesis_config,
2368            mint_keypair,
2369            ..
2370        } = create_genesis_config(10_000);
2371
2372        let bank = Bank::new_for_tests(&genesis_config);
2373        let (bank, _bank_forks) = setup_dummy_fork_graph(bank);
2374        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2375        let pool = SchedulerPool::<PooledScheduler<CountingHandler>, _>::new(
2376            None,
2377            None,
2378            None,
2379            None,
2380            ignored_prioritization_fee_cache,
2381        );
2382        let context = SchedulingContext::new(bank.clone());
2383        let scheduler = pool.do_take_scheduler(context);
2384
2385        // This test is racy.
2386        //
2387        // That's because the scheduler needs to be aborted quickly as an expected behavior,
2388        // leaving some readily-available work untouched. So, schedule rather large number of tasks
2389        // to make the short-cutting abort code-path win the race easily.
2390        const MAX_TASK_COUNT: usize = 100;
2391
2392        for i in 0..MAX_TASK_COUNT {
2393            let tx = RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
2394                &mint_keypair,
2395                &clone_solana_pubkey::new_rand(),
2396                2,
2397                genesis_config.hash(),
2398            ));
2399            scheduler.schedule_execution(tx, i).unwrap();
2400        }
2401
2402        // Make sure ThreadManager::drop() is properly short-circuiting for non-aborting scheduler.
2403        sleepless_testing::at(TestCheckPoint::BeforeThreadManagerDrop);
2404        drop::<PooledScheduler<_>>(scheduler);
2405        sleepless_testing::at(TestCheckPoint::AfterSchedulerThreadAborted);
2406        // All of handler threads should have been aborted before processing MAX_TASK_COUNT tasks.
2407        assert!(*TASK_COUNT.lock().unwrap() < MAX_TASK_COUNT);
2408    }
2409
2410    #[test]
2411    fn test_scheduler_pool_filo() {
2412        clone_solana_logger::setup();
2413
2414        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2415        let pool =
2416            DefaultSchedulerPool::new(None, None, None, None, ignored_prioritization_fee_cache);
2417        let bank = Arc::new(Bank::default_for_tests());
2418        let context = &SchedulingContext::new(bank);
2419
2420        let scheduler1 = pool.do_take_scheduler(context.clone());
2421        let scheduler_id1 = scheduler1.id();
2422        let scheduler2 = pool.do_take_scheduler(context.clone());
2423        let scheduler_id2 = scheduler2.id();
2424        assert_ne!(scheduler_id1, scheduler_id2);
2425
2426        let (result_with_timings, scheduler1) = scheduler1.into_inner();
2427        assert_matches!(result_with_timings, (Ok(()), _));
2428        pool.return_scheduler(scheduler1);
2429        let (result_with_timings, scheduler2) = scheduler2.into_inner();
2430        assert_matches!(result_with_timings, (Ok(()), _));
2431        pool.return_scheduler(scheduler2);
2432
2433        let scheduler3 = pool.do_take_scheduler(context.clone());
2434        assert_eq!(scheduler_id2, scheduler3.id());
2435        let scheduler4 = pool.do_take_scheduler(context.clone());
2436        assert_eq!(scheduler_id1, scheduler4.id());
2437    }
2438
2439    #[test]
2440    fn test_scheduler_pool_context_drop_unless_reinitialized() {
2441        clone_solana_logger::setup();
2442
2443        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2444        let pool =
2445            DefaultSchedulerPool::new(None, None, None, None, ignored_prioritization_fee_cache);
2446        let bank = Arc::new(Bank::default_for_tests());
2447        let context = &SchedulingContext::new(bank);
2448        let mut scheduler = pool.do_take_scheduler(context.clone());
2449
2450        // should never panic.
2451        scheduler.pause_for_recent_blockhash();
2452        assert_matches!(
2453            Box::new(scheduler).wait_for_termination(false),
2454            ((Ok(()), _), _)
2455        );
2456    }
2457
2458    #[test]
2459    fn test_scheduler_pool_context_replace() {
2460        clone_solana_logger::setup();
2461
2462        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2463        let pool =
2464            DefaultSchedulerPool::new(None, None, None, None, ignored_prioritization_fee_cache);
2465        let old_bank = &Arc::new(Bank::default_for_tests());
2466        let new_bank = &Arc::new(Bank::default_for_tests());
2467        assert!(!Arc::ptr_eq(old_bank, new_bank));
2468
2469        let old_context = &SchedulingContext::new(old_bank.clone());
2470        let new_context = &SchedulingContext::new(new_bank.clone());
2471
2472        let scheduler = pool.do_take_scheduler(old_context.clone());
2473        let scheduler_id = scheduler.id();
2474        pool.return_scheduler(scheduler.into_inner().1);
2475
2476        let scheduler = pool.take_scheduler(new_context.clone());
2477        assert_eq!(scheduler_id, scheduler.id());
2478        assert!(Arc::ptr_eq(scheduler.context().bank(), new_bank));
2479    }
2480
2481    #[test]
2482    fn test_scheduler_pool_install_into_bank_forks() {
2483        clone_solana_logger::setup();
2484
2485        let bank = Bank::default_for_tests();
2486        let bank_forks = BankForks::new_rw_arc(bank);
2487        let mut bank_forks = bank_forks.write().unwrap();
2488        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2489        let pool =
2490            DefaultSchedulerPool::new_dyn(None, None, None, None, ignored_prioritization_fee_cache);
2491        bank_forks.install_scheduler_pool(pool);
2492    }
2493
2494    #[test]
2495    fn test_scheduler_install_into_bank() {
2496        clone_solana_logger::setup();
2497
2498        let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(10_000);
2499        let bank = Arc::new(Bank::new_for_tests(&genesis_config));
2500        let child_bank = Bank::new_from_parent(bank, &Pubkey::default(), 1);
2501
2502        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2503        let pool =
2504            DefaultSchedulerPool::new_dyn(None, None, None, None, ignored_prioritization_fee_cache);
2505
2506        let bank = Bank::default_for_tests();
2507        let bank_forks = BankForks::new_rw_arc(bank);
2508        let mut bank_forks = bank_forks.write().unwrap();
2509
2510        // existing banks in bank_forks shouldn't process transactions anymore in general, so
2511        // shouldn't be touched
2512        assert!(!bank_forks
2513            .working_bank_with_scheduler()
2514            .has_installed_scheduler());
2515        bank_forks.install_scheduler_pool(pool);
2516        assert!(!bank_forks
2517            .working_bank_with_scheduler()
2518            .has_installed_scheduler());
2519
2520        let mut child_bank = bank_forks.insert(child_bank);
2521        assert!(child_bank.has_installed_scheduler());
2522        bank_forks.remove(child_bank.slot());
2523        child_bank.drop_scheduler();
2524        assert!(!child_bank.has_installed_scheduler());
2525    }
2526
2527    fn setup_dummy_fork_graph(bank: Bank) -> (Arc<Bank>, Arc<RwLock<BankForks>>) {
2528        let slot = bank.slot();
2529        let bank_fork = BankForks::new_rw_arc(bank);
2530        let bank = bank_fork.read().unwrap().get(slot).unwrap();
2531        bank.set_fork_graph_in_program_cache(Arc::downgrade(&bank_fork));
2532        (bank, bank_fork)
2533    }
2534
2535    #[test]
2536    fn test_scheduler_schedule_execution_success() {
2537        clone_solana_logger::setup();
2538
2539        let GenesisConfigInfo {
2540            genesis_config,
2541            mint_keypair,
2542            ..
2543        } = create_genesis_config(10_000);
2544        let tx0 = RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
2545            &mint_keypair,
2546            &clone_solana_pubkey::new_rand(),
2547            2,
2548            genesis_config.hash(),
2549        ));
2550        let bank = Bank::new_for_tests(&genesis_config);
2551        let (bank, _bank_forks) = setup_dummy_fork_graph(bank);
2552        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2553        let pool =
2554            DefaultSchedulerPool::new_dyn(None, None, None, None, ignored_prioritization_fee_cache);
2555        let context = SchedulingContext::new(bank.clone());
2556
2557        assert_eq!(bank.transaction_count(), 0);
2558        let scheduler = pool.take_scheduler(context);
2559        scheduler.schedule_execution(tx0, 0).unwrap();
2560        let bank = BankWithScheduler::new(bank, Some(scheduler));
2561        assert_matches!(bank.wait_for_completed_scheduler(), Some((Ok(()), _)));
2562        assert_eq!(bank.transaction_count(), 1);
2563    }
2564
2565    fn do_test_scheduler_schedule_execution_failure(extra_tx_after_failure: bool) {
2566        clone_solana_logger::setup();
2567
2568        let _progress = sleepless_testing::setup(&[
2569            &CheckPoint::TaskHandled(0),
2570            &TestCheckPoint::AfterTaskHandled,
2571            &CheckPoint::SchedulerThreadAborted,
2572            &TestCheckPoint::AfterSchedulerThreadAborted,
2573            &TestCheckPoint::BeforeTrashedSchedulerCleaned,
2574            &CheckPoint::TrashedSchedulerCleaned(0),
2575            &CheckPoint::TrashedSchedulerCleaned(1),
2576            &TestCheckPoint::AfterTrashedSchedulerCleaned,
2577        ]);
2578
2579        let GenesisConfigInfo {
2580            genesis_config,
2581            mint_keypair,
2582            ..
2583        } = create_genesis_config(10_000);
2584        let bank = Bank::new_for_tests(&genesis_config);
2585        let (bank, _bank_forks) = setup_dummy_fork_graph(bank);
2586
2587        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2588        let pool_raw = DefaultSchedulerPool::do_new(
2589            None,
2590            None,
2591            None,
2592            None,
2593            ignored_prioritization_fee_cache,
2594            SHORTENED_POOL_CLEANER_INTERVAL,
2595            DEFAULT_MAX_POOLING_DURATION,
2596            DEFAULT_MAX_USAGE_QUEUE_COUNT,
2597            DEFAULT_TIMEOUT_DURATION,
2598        );
2599        let pool = pool_raw.clone();
2600        let context = SchedulingContext::new(bank.clone());
2601        let scheduler = pool.take_scheduler(context);
2602
2603        let unfunded_keypair = Keypair::new();
2604        let bad_tx = RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
2605            &unfunded_keypair,
2606            &clone_solana_pubkey::new_rand(),
2607            2,
2608            genesis_config.hash(),
2609        ));
2610        assert_eq!(bank.transaction_count(), 0);
2611        scheduler.schedule_execution(bad_tx, 0).unwrap();
2612        sleepless_testing::at(TestCheckPoint::AfterTaskHandled);
2613        assert_eq!(bank.transaction_count(), 0);
2614
2615        let good_tx_after_bad_tx =
2616            RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
2617                &mint_keypair,
2618                &clone_solana_pubkey::new_rand(),
2619                3,
2620                genesis_config.hash(),
2621            ));
2622        // make sure this tx is really a good one to execute.
2623        assert_matches!(
2624            bank.simulate_transaction_unchecked(&good_tx_after_bad_tx, false)
2625                .result,
2626            Ok(_)
2627        );
2628        sleepless_testing::at(TestCheckPoint::AfterSchedulerThreadAborted);
2629        let bank = BankWithScheduler::new(bank, Some(scheduler));
2630        if extra_tx_after_failure {
2631            assert_matches!(
2632                bank.schedule_transaction_executions([(good_tx_after_bad_tx, 1)].into_iter()),
2633                Err(TransactionError::AccountNotFound)
2634            );
2635        }
2636        // transaction_count should remain same as scheduler should be bailing out.
2637        // That's because we're testing the serialized failing execution case in this test.
2638        // Also note that bank.transaction_count() is generally racy by nature, because
2639        // blockstore_processor and unified_scheduler both tend to process non-conflicting batches
2640        // in parallel as part of the normal operation.
2641        assert_eq!(bank.transaction_count(), 0);
2642
2643        assert_eq!(pool_raw.trashed_scheduler_inners.lock().unwrap().len(), 0);
2644        assert_matches!(
2645            bank.wait_for_completed_scheduler(),
2646            Some((Err(TransactionError::AccountNotFound), _timings))
2647        );
2648
2649        // Block solScCleaner until we see trashed schedler...
2650        assert_eq!(pool_raw.trashed_scheduler_inners.lock().unwrap().len(), 1);
2651        sleepless_testing::at(TestCheckPoint::BeforeTrashedSchedulerCleaned);
2652
2653        // See the trashed scheduler gone only after solScCleaner did its job...
2654        sleepless_testing::at(TestCheckPoint::AfterTrashedSchedulerCleaned);
2655        assert_eq!(pool_raw.trashed_scheduler_inners.lock().unwrap().len(), 0);
2656    }
2657
2658    #[test]
2659    fn test_scheduler_schedule_execution_failure_with_extra_tx() {
2660        do_test_scheduler_schedule_execution_failure(true);
2661    }
2662
2663    #[test]
2664    fn test_scheduler_schedule_execution_failure_without_extra_tx() {
2665        do_test_scheduler_schedule_execution_failure(false);
2666    }
2667
2668    #[test]
2669    #[should_panic(expected = "This panic should be propagated. (From: ")]
2670    fn test_scheduler_schedule_execution_panic() {
2671        clone_solana_logger::setup();
2672
2673        #[derive(Debug)]
2674        enum PanickingHanlderCheckPoint {
2675            BeforeNotifiedPanic,
2676            BeforeIgnoredPanic,
2677        }
2678
2679        let progress = sleepless_testing::setup(&[
2680            &TestCheckPoint::BeforeNewTask,
2681            &CheckPoint::NewTask(0),
2682            &PanickingHanlderCheckPoint::BeforeNotifiedPanic,
2683            &CheckPoint::SchedulerThreadAborted,
2684            &PanickingHanlderCheckPoint::BeforeIgnoredPanic,
2685            &TestCheckPoint::BeforeEndSession,
2686        ]);
2687
2688        #[derive(Debug)]
2689        struct PanickingHandler;
2690        impl TaskHandler for PanickingHandler {
2691            fn handle(
2692                _result: &mut Result<()>,
2693                _timings: &mut ExecuteTimings,
2694                _bank: &SchedulingContext,
2695                task: &Task,
2696                _handler_context: &HandlerContext,
2697            ) {
2698                let index = task.task_index();
2699                if index == 0 {
2700                    sleepless_testing::at(PanickingHanlderCheckPoint::BeforeNotifiedPanic);
2701                } else if index == 1 {
2702                    sleepless_testing::at(PanickingHanlderCheckPoint::BeforeIgnoredPanic);
2703                } else {
2704                    unreachable!();
2705                }
2706                panic!("This panic should be propagated.");
2707            }
2708        }
2709
2710        let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(10_000);
2711
2712        let bank = Bank::new_for_tests(&genesis_config);
2713        let (bank, _bank_forks) = setup_dummy_fork_graph(bank);
2714
2715        // Use 2 transactions with different timings to deliberately cover the two code paths of
2716        // notifying panics in the handler threads, taken conditionally depending on whether the
2717        // scheduler thread has been aborted already or not.
2718        const TX_COUNT: usize = 2;
2719
2720        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2721        let pool = SchedulerPool::<PooledScheduler<PanickingHandler>, _>::new_dyn(
2722            Some(TX_COUNT), // fix to use exactly 2 handlers
2723            None,
2724            None,
2725            None,
2726            ignored_prioritization_fee_cache,
2727        );
2728        let context = SchedulingContext::new(bank.clone());
2729
2730        let scheduler = pool.take_scheduler(context);
2731
2732        for index in 0..TX_COUNT {
2733            // Use 2 non-conflicting txes to exercise the channel disconnected case as well.
2734            let tx = RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
2735                &Keypair::new(),
2736                &clone_solana_pubkey::new_rand(),
2737                1,
2738                genesis_config.hash(),
2739            ));
2740            scheduler.schedule_execution(tx, index).unwrap();
2741        }
2742        // finally unblock the scheduler thread; otherwise the above schedule_execution could
2743        // return SchedulerAborted...
2744        sleepless_testing::at(TestCheckPoint::BeforeNewTask);
2745
2746        sleepless_testing::at(TestCheckPoint::BeforeEndSession);
2747        let bank = BankWithScheduler::new(bank, Some(scheduler));
2748
2749        // the outer .unwrap() will panic. so, drop progress now.
2750        drop(progress);
2751        bank.wait_for_completed_scheduler().unwrap().0.unwrap();
2752    }
2753
2754    #[test]
2755    fn test_scheduler_execution_failure_short_circuiting() {
2756        clone_solana_logger::setup();
2757
2758        let _progress = sleepless_testing::setup(&[
2759            &TestCheckPoint::BeforeNewTask,
2760            &CheckPoint::NewTask(0),
2761            &CheckPoint::TaskHandled(0),
2762            &CheckPoint::SchedulerThreadAborted,
2763            &TestCheckPoint::AfterSchedulerThreadAborted,
2764        ]);
2765
2766        static TASK_COUNT: Mutex<usize> = Mutex::new(0);
2767
2768        #[derive(Debug)]
2769        struct CountingFaultyHandler;
2770        impl TaskHandler for CountingFaultyHandler {
2771            fn handle(
2772                result: &mut Result<()>,
2773                _timings: &mut ExecuteTimings,
2774                _bank: &SchedulingContext,
2775                task: &Task,
2776                _handler_context: &HandlerContext,
2777            ) {
2778                let index = task.task_index();
2779                *TASK_COUNT.lock().unwrap() += 1;
2780                if index == 1 {
2781                    *result = Err(TransactionError::AccountNotFound);
2782                }
2783                sleepless_testing::at(CheckPoint::TaskHandled(index));
2784            }
2785        }
2786
2787        let GenesisConfigInfo {
2788            genesis_config,
2789            mint_keypair,
2790            ..
2791        } = create_genesis_config(10_000);
2792
2793        let bank = Bank::new_for_tests(&genesis_config);
2794        let (bank, _bank_forks) = setup_dummy_fork_graph(bank);
2795        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2796        let pool = SchedulerPool::<PooledScheduler<CountingFaultyHandler>, _>::new(
2797            None,
2798            None,
2799            None,
2800            None,
2801            ignored_prioritization_fee_cache,
2802        );
2803        let context = SchedulingContext::new(bank.clone());
2804        let scheduler = pool.do_take_scheduler(context);
2805
2806        for i in 0..10 {
2807            let tx = RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
2808                &mint_keypair,
2809                &clone_solana_pubkey::new_rand(),
2810                2,
2811                genesis_config.hash(),
2812            ));
2813            scheduler.schedule_execution(tx, i).unwrap();
2814        }
2815        // finally unblock the scheduler thread; otherwise the above schedule_execution could
2816        // return SchedulerAborted...
2817        sleepless_testing::at(TestCheckPoint::BeforeNewTask);
2818
2819        // Make sure bank.wait_for_completed_scheduler() is properly short-circuiting for aborting scheduler.
2820        let bank = BankWithScheduler::new(bank, Some(Box::new(scheduler)));
2821        assert_matches!(
2822            bank.wait_for_completed_scheduler(),
2823            Some((Err(TransactionError::AccountNotFound), _timings))
2824        );
2825        sleepless_testing::at(TestCheckPoint::AfterSchedulerThreadAborted);
2826        assert!(*TASK_COUNT.lock().unwrap() < 10);
2827    }
2828
2829    #[test]
2830    fn test_scheduler_schedule_execution_blocked() {
2831        clone_solana_logger::setup();
2832
2833        const STALLED_TRANSACTION_INDEX: usize = 0;
2834        const BLOCKED_TRANSACTION_INDEX: usize = 1;
2835        static LOCK_TO_STALL: Mutex<()> = Mutex::new(());
2836
2837        #[derive(Debug)]
2838        struct StallingHandler;
2839        impl TaskHandler for StallingHandler {
2840            fn handle(
2841                result: &mut Result<()>,
2842                timings: &mut ExecuteTimings,
2843                bank: &SchedulingContext,
2844                task: &Task,
2845                handler_context: &HandlerContext,
2846            ) {
2847                let index = task.task_index();
2848                match index {
2849                    STALLED_TRANSACTION_INDEX => *LOCK_TO_STALL.lock().unwrap(),
2850                    BLOCKED_TRANSACTION_INDEX => {}
2851                    _ => unreachable!(),
2852                };
2853                DefaultTaskHandler::handle(result, timings, bank, task, handler_context);
2854            }
2855        }
2856
2857        let GenesisConfigInfo {
2858            genesis_config,
2859            mint_keypair,
2860            ..
2861        } = create_genesis_config(10_000);
2862
2863        // tx0 and tx1 is definitely conflicting to write-lock the mint address
2864        let tx0 = RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
2865            &mint_keypair,
2866            &clone_solana_pubkey::new_rand(),
2867            2,
2868            genesis_config.hash(),
2869        ));
2870        let tx1 = RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
2871            &mint_keypair,
2872            &clone_solana_pubkey::new_rand(),
2873            2,
2874            genesis_config.hash(),
2875        ));
2876
2877        let bank = Bank::new_for_tests(&genesis_config);
2878        let (bank, _bank_forks) = setup_dummy_fork_graph(bank);
2879        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2880        let pool = SchedulerPool::<PooledScheduler<StallingHandler>, _>::new_dyn(
2881            None,
2882            None,
2883            None,
2884            None,
2885            ignored_prioritization_fee_cache,
2886        );
2887        let context = SchedulingContext::new(bank.clone());
2888
2889        assert_eq!(bank.transaction_count(), 0);
2890        let scheduler = pool.take_scheduler(context);
2891
2892        // Stall handling tx0 and tx1
2893        let lock_to_stall = LOCK_TO_STALL.lock().unwrap();
2894        scheduler
2895            .schedule_execution(tx0, STALLED_TRANSACTION_INDEX)
2896            .unwrap();
2897        scheduler
2898            .schedule_execution(tx1, BLOCKED_TRANSACTION_INDEX)
2899            .unwrap();
2900
2901        // Wait a bit for the scheduler thread to decide to block tx1
2902        std::thread::sleep(std::time::Duration::from_secs(1));
2903
2904        // Resume handling by unlocking LOCK_TO_STALL
2905        drop(lock_to_stall);
2906        let bank = BankWithScheduler::new(bank, Some(scheduler));
2907        assert_matches!(bank.wait_for_completed_scheduler(), Some((Ok(()), _)));
2908        assert_eq!(bank.transaction_count(), 2);
2909    }
2910
2911    #[test]
2912    fn test_scheduler_mismatched_scheduling_context_race() {
2913        clone_solana_logger::setup();
2914
2915        #[derive(Debug)]
2916        struct TaskAndContextChecker;
2917        impl TaskHandler for TaskAndContextChecker {
2918            fn handle(
2919                _result: &mut Result<()>,
2920                _timings: &mut ExecuteTimings,
2921                context: &SchedulingContext,
2922                task: &Task,
2923                _handler_context: &HandlerContext,
2924            ) {
2925                // The task index must always be matched to the slot.
2926                assert_eq!(task.task_index() as Slot, context.bank().slot());
2927            }
2928        }
2929
2930        let GenesisConfigInfo {
2931            genesis_config,
2932            mint_keypair,
2933            ..
2934        } = create_genesis_config(10_000);
2935
2936        // Create two banks for two contexts
2937        let bank0 = Bank::new_for_tests(&genesis_config);
2938        let bank0 = setup_dummy_fork_graph(bank0).0;
2939        let bank1 = Arc::new(Bank::new_from_parent(
2940            bank0.clone(),
2941            &Pubkey::default(),
2942            bank0.slot().checked_add(1).unwrap(),
2943        ));
2944
2945        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
2946        let pool = SchedulerPool::<PooledScheduler<TaskAndContextChecker>, _>::new(
2947            Some(4), // spawn 4 threads
2948            None,
2949            None,
2950            None,
2951            ignored_prioritization_fee_cache,
2952        );
2953
2954        // Create a dummy tx and two contexts
2955        let dummy_tx =
2956            RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
2957                &mint_keypair,
2958                &clone_solana_pubkey::new_rand(),
2959                2,
2960                genesis_config.hash(),
2961            ));
2962        let context0 = &SchedulingContext::new(bank0.clone());
2963        let context1 = &SchedulingContext::new(bank1.clone());
2964
2965        // Exercise the scheduler by busy-looping to expose the race condition
2966        for (context, index) in [(context0, 0), (context1, 1)]
2967            .into_iter()
2968            .cycle()
2969            .take(10000)
2970        {
2971            let scheduler = pool.take_scheduler(context.clone());
2972            scheduler
2973                .schedule_execution(dummy_tx.clone(), index)
2974                .unwrap();
2975            scheduler.wait_for_termination(false).1.return_to_pool();
2976        }
2977    }
2978
2979    #[derive(Debug)]
2980    struct AsyncScheduler<const TRIGGER_RACE_CONDITION: bool>(
2981        Mutex<ResultWithTimings>,
2982        Mutex<Vec<JoinHandle<ResultWithTimings>>>,
2983        SchedulingContext,
2984        Arc<SchedulerPool<Self, DefaultTaskHandler>>,
2985    );
2986
2987    impl<const TRIGGER_RACE_CONDITION: bool> AsyncScheduler<TRIGGER_RACE_CONDITION> {
2988        fn do_wait(&self) {
2989            let mut overall_result = Ok(());
2990            let mut overall_timings = ExecuteTimings::default();
2991            for handle in self.1.lock().unwrap().drain(..) {
2992                let (result, timings) = handle.join().unwrap();
2993                match result {
2994                    Ok(()) => {}
2995                    Err(e) => overall_result = Err(e),
2996                }
2997                overall_timings.accumulate(&timings);
2998            }
2999            *self.0.lock().unwrap() = (overall_result, overall_timings);
3000        }
3001    }
3002
3003    impl<const TRIGGER_RACE_CONDITION: bool> InstalledScheduler
3004        for AsyncScheduler<TRIGGER_RACE_CONDITION>
3005    {
3006        fn id(&self) -> SchedulerId {
3007            unimplemented!();
3008        }
3009
3010        fn context(&self) -> &SchedulingContext {
3011            &self.2
3012        }
3013
3014        fn schedule_execution(
3015            &self,
3016            transaction: RuntimeTransaction<SanitizedTransaction>,
3017            index: usize,
3018        ) -> ScheduleResult {
3019            let context = self.context().clone();
3020            let pool = self.3.clone();
3021
3022            self.1.lock().unwrap().push(std::thread::spawn(move || {
3023                // intentionally sleep to simulate race condition where register_recent_blockhash
3024                // is handle before finishing executing scheduled transactions
3025                std::thread::sleep(std::time::Duration::from_secs(1));
3026
3027                let mut result = Ok(());
3028                let mut timings = ExecuteTimings::default();
3029
3030                let task = SchedulingStateMachine::create_task(transaction, index, &mut |_| {
3031                    UsageQueue::default()
3032                });
3033
3034                <DefaultTaskHandler as TaskHandler>::handle(
3035                    &mut result,
3036                    &mut timings,
3037                    &context,
3038                    &task,
3039                    &pool.create_handler_context(
3040                        BlockVerification,
3041                        &crossbeam_channel::unbounded().0,
3042                    ),
3043                );
3044                (result, timings)
3045            }));
3046
3047            Ok(())
3048        }
3049
3050        fn recover_error_after_abort(&mut self) -> TransactionError {
3051            unimplemented!();
3052        }
3053
3054        fn wait_for_termination(
3055            self: Box<Self>,
3056            _is_dropped: bool,
3057        ) -> (ResultWithTimings, UninstalledSchedulerBox) {
3058            self.do_wait();
3059            let result_with_timings = std::mem::replace(
3060                &mut *self.0.lock().unwrap(),
3061                initialized_result_with_timings(),
3062            );
3063            (result_with_timings, self)
3064        }
3065
3066        fn pause_for_recent_blockhash(&mut self) {
3067            if TRIGGER_RACE_CONDITION {
3068                // this is equivalent to NOT calling wait_for_paused_scheduler() in
3069                // register_recent_blockhash().
3070                return;
3071            }
3072            self.do_wait();
3073        }
3074    }
3075
3076    impl<const TRIGGER_RACE_CONDITION: bool> SchedulerInner for AsyncScheduler<TRIGGER_RACE_CONDITION> {
3077        fn id(&self) -> SchedulerId {
3078            42
3079        }
3080
3081        fn is_trashed(&self) -> bool {
3082            false
3083        }
3084    }
3085
3086    impl<const TRIGGER_RACE_CONDITION: bool> UninstalledScheduler
3087        for AsyncScheduler<TRIGGER_RACE_CONDITION>
3088    {
3089        fn return_to_pool(self: Box<Self>) {
3090            self.3.clone().return_scheduler(*self)
3091        }
3092    }
3093
3094    impl<const TRIGGER_RACE_CONDITION: bool> SpawnableScheduler<DefaultTaskHandler>
3095        for AsyncScheduler<TRIGGER_RACE_CONDITION>
3096    {
3097        // well, i wish i can use ! (never type).....
3098        type Inner = Self;
3099
3100        fn into_inner(self) -> (ResultWithTimings, Self::Inner) {
3101            unimplemented!();
3102        }
3103
3104        fn from_inner(
3105            _inner: Self::Inner,
3106            _context: SchedulingContext,
3107            _result_with_timings: ResultWithTimings,
3108        ) -> Self {
3109            unimplemented!();
3110        }
3111
3112        fn spawn(
3113            pool: Arc<SchedulerPool<Self, DefaultTaskHandler>>,
3114            context: SchedulingContext,
3115            _result_with_timings: ResultWithTimings,
3116        ) -> Self {
3117            AsyncScheduler::<TRIGGER_RACE_CONDITION>(
3118                Mutex::new(initialized_result_with_timings()),
3119                Mutex::new(vec![]),
3120                context,
3121                pool,
3122            )
3123        }
3124    }
3125
3126    fn do_test_scheduler_schedule_execution_recent_blockhash_edge_case<
3127        const TRIGGER_RACE_CONDITION: bool,
3128    >() {
3129        clone_solana_logger::setup();
3130
3131        let GenesisConfigInfo {
3132            genesis_config,
3133            mint_keypair,
3134            ..
3135        } = create_genesis_config(10_000);
3136        let very_old_valid_tx =
3137            RuntimeTransaction::from_transaction_for_tests(system_transaction::transfer(
3138                &mint_keypair,
3139                &clone_solana_pubkey::new_rand(),
3140                2,
3141                genesis_config.hash(),
3142            ));
3143        let mut bank = Bank::new_for_tests(&genesis_config);
3144        for _ in 0..MAX_PROCESSING_AGE {
3145            bank.fill_bank_with_ticks_for_tests();
3146            bank.freeze();
3147            let slot = bank.slot();
3148            bank = Bank::new_from_parent(
3149                Arc::new(bank),
3150                &Pubkey::default(),
3151                slot.checked_add(1).unwrap(),
3152            );
3153        }
3154        let (bank, _bank_forks) = setup_dummy_fork_graph(bank);
3155        let context = SchedulingContext::new(bank.clone());
3156
3157        let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
3158        let pool =
3159            SchedulerPool::<AsyncScheduler<TRIGGER_RACE_CONDITION>, DefaultTaskHandler>::new_dyn(
3160                None,
3161                None,
3162                None,
3163                None,
3164                ignored_prioritization_fee_cache,
3165            );
3166        let scheduler = pool.take_scheduler(context);
3167
3168        let bank = BankWithScheduler::new(bank, Some(scheduler));
3169        assert_eq!(bank.transaction_count(), 0);
3170
3171        // schedule but not immediately execute transaction
3172        bank.schedule_transaction_executions([(very_old_valid_tx, 0)].into_iter())
3173            .unwrap();
3174        // this calls register_recent_blockhash internally
3175        bank.fill_bank_with_ticks_for_tests();
3176
3177        if TRIGGER_RACE_CONDITION {
3178            // very_old_valid_tx is wrongly handled as expired!
3179            assert_matches!(
3180                bank.wait_for_completed_scheduler(),
3181                Some((Err(TransactionError::BlockhashNotFound), _))
3182            );
3183            assert_eq!(bank.transaction_count(), 0);
3184        } else {
3185            assert_matches!(bank.wait_for_completed_scheduler(), Some((Ok(()), _)));
3186            assert_eq!(bank.transaction_count(), 1);
3187        }
3188    }
3189
3190    #[test]
3191    fn test_scheduler_schedule_execution_recent_blockhash_edge_case_with_race() {
3192        do_test_scheduler_schedule_execution_recent_blockhash_edge_case::<true>();
3193    }
3194
3195    #[test]
3196    fn test_scheduler_schedule_execution_recent_blockhash_edge_case_without_race() {
3197        do_test_scheduler_schedule_execution_recent_blockhash_edge_case::<false>();
3198    }
3199
3200    #[test]
3201    fn test_default_handler_count() {
3202        for (detected, expected) in [(32, 8), (4, 1), (2, 1)] {
3203            assert_eq!(
3204                DefaultSchedulerPool::calculate_default_handler_count(Some(detected)),
3205                expected
3206            );
3207        }
3208        assert_eq!(
3209            DefaultSchedulerPool::calculate_default_handler_count(None),
3210            4
3211        );
3212    }
3213
3214    // See comment in SchedulingStateMachine::create_task() for the justification of this test
3215    #[test]
3216    fn test_enfoced_get_account_locks_validation() {
3217        clone_solana_logger::setup();
3218
3219        let GenesisConfigInfo {
3220            genesis_config,
3221            ref mint_keypair,
3222            ..
3223        } = create_genesis_config(10_000);
3224        let bank = Bank::new_for_tests(&genesis_config);
3225        let (bank, _bank_forks) = &setup_dummy_fork_graph(bank);
3226
3227        let mut tx = system_transaction::transfer(
3228            mint_keypair,
3229            &clone_solana_pubkey::new_rand(),
3230            2,
3231            genesis_config.hash(),
3232        );
3233        // mangle the transfer tx to try to lock fee_payer (= mint_keypair) address twice!
3234        tx.message.account_keys.push(tx.message.account_keys[0]);
3235        let tx = RuntimeTransaction::from_transaction_for_tests(tx);
3236
3237        // this internally should call SanitizedTransaction::get_account_locks().
3238        let result = &mut Ok(());
3239        let timings = &mut ExecuteTimings::default();
3240        let prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
3241        let scheduling_context = &SchedulingContext::new(bank.clone());
3242        let handler_context = &HandlerContext {
3243            log_messages_bytes_limit: None,
3244            transaction_status_sender: None,
3245            replay_vote_sender: None,
3246            prioritization_fee_cache,
3247            banking_packet_receiver: never(),
3248            banking_packet_handler: Box::new(|_, _| {}),
3249            banking_stage_helper: None,
3250            transaction_recorder: None,
3251        };
3252
3253        let task = SchedulingStateMachine::create_task(tx, 0, &mut |_| UsageQueue::default());
3254        DefaultTaskHandler::handle(result, timings, scheduling_context, &task, handler_context);
3255        assert_matches!(result, Err(TransactionError::AccountLoadedTwice));
3256    }
3257
3258    enum TxResult {
3259        ExecutedWithSuccess,
3260        ExecutedWithFailure,
3261        NotExecuted,
3262    }
3263
3264    #[test_matrix(
3265        [TxResult::ExecutedWithSuccess, TxResult::ExecutedWithFailure, TxResult::NotExecuted],
3266        [false, true]
3267    )]
3268    fn test_task_handler_poh_recording(tx_result: TxResult, should_succeed_to_record_to_poh: bool) {
3269        clone_solana_logger::setup();
3270
3271        let GenesisConfigInfo {
3272            genesis_config,
3273            ref mint_keypair,
3274            ..
3275        } = clone_solana_ledger::genesis_utils::create_genesis_config(10_000);
3276        let bank = Bank::new_for_tests(&genesis_config);
3277        let bank_forks = BankForks::new_rw_arc(bank);
3278        let bank = bank_forks.read().unwrap().working_bank_with_scheduler();
3279
3280        let (tx, expected_tx_result) = match tx_result {
3281            TxResult::ExecutedWithSuccess => (
3282                system_transaction::transfer(
3283                    mint_keypair,
3284                    &clone_solana_pubkey::new_rand(),
3285                    1,
3286                    genesis_config.hash(),
3287                ),
3288                Ok(()),
3289            ),
3290            TxResult::ExecutedWithFailure => (
3291                system_transaction::transfer(
3292                    mint_keypair,
3293                    &clone_solana_pubkey::new_rand(),
3294                    1_000_000,
3295                    genesis_config.hash(),
3296                ),
3297                Ok(()),
3298            ),
3299            TxResult::NotExecuted => (
3300                system_transaction::transfer(
3301                    mint_keypair,
3302                    &clone_solana_pubkey::new_rand(),
3303                    1,
3304                    Hash::default(),
3305                ),
3306                Err(TransactionError::BlockhashNotFound),
3307            ),
3308        };
3309        let tx = RuntimeTransaction::from_transaction_for_tests(tx);
3310
3311        let result = &mut Ok(());
3312        let timings = &mut ExecuteTimings::default();
3313        let prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64));
3314        let scheduling_context = &SchedulingContext::for_production(bank.clone());
3315        let (sender, receiver) = crossbeam_channel::unbounded();
3316        let (ledger_path, _blockhash) = create_new_tmp_ledger_auto_delete!(&genesis_config);
3317        let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap());
3318        let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank));
3319        let (exit, poh_recorder, poh_service, signal_receiver) =
3320            create_test_recorder_with_index_tracking(
3321                bank.clone(),
3322                blockstore.clone(),
3323                None,
3324                Some(leader_schedule_cache),
3325            );
3326        let handler_context = &HandlerContext {
3327            log_messages_bytes_limit: None,
3328            transaction_status_sender: Some(TransactionStatusSender { sender }),
3329            replay_vote_sender: None,
3330            prioritization_fee_cache,
3331            banking_packet_receiver: never(),
3332            banking_packet_handler: Box::new(|_, _| {}),
3333            banking_stage_helper: None,
3334            transaction_recorder: Some(poh_recorder.read().unwrap().new_recorder()),
3335        };
3336
3337        let task =
3338            SchedulingStateMachine::create_task(tx.clone(), 0, &mut |_| UsageQueue::default());
3339
3340        // wait until the poh's working bank is cleared.
3341        // also flush signal_receiver after that.
3342        if !should_succeed_to_record_to_poh {
3343            while poh_recorder.read().unwrap().bank().is_some() {
3344                sleep(Duration::from_millis(100));
3345            }
3346            while signal_receiver.try_recv().is_ok() {}
3347        }
3348
3349        assert_eq!(bank.transaction_count(), 0);
3350        assert_eq!(bank.transaction_error_count(), 0);
3351        DefaultTaskHandler::handle(result, timings, scheduling_context, &task, handler_context);
3352
3353        if should_succeed_to_record_to_poh {
3354            if expected_tx_result.is_ok() {
3355                assert_matches!(result, Ok(()));
3356                assert_eq!(bank.transaction_count(), 1);
3357                if matches!(tx_result, TxResult::ExecutedWithFailure) {
3358                    assert_eq!(bank.transaction_error_count(), 1);
3359                } else {
3360                    assert_eq!(bank.transaction_error_count(), 0);
3361                }
3362                assert_matches!(
3363                    receiver.try_recv(),
3364                    Ok(TransactionStatusMessage::Batch(
3365                        TransactionStatusBatch { .. }
3366                    ))
3367                );
3368                assert_matches!(
3369                    signal_receiver.try_recv(),
3370                    Ok((_, (clone_solana_entry::entry::Entry {transactions, ..} , _)))
3371                        if transactions == vec![tx.to_versioned_transaction()]
3372                );
3373            } else {
3374                assert_eq!(result, &expected_tx_result);
3375                assert_eq!(bank.transaction_count(), 0);
3376                assert_eq!(bank.transaction_error_count(), 0);
3377                assert_matches!(receiver.try_recv(), Err(_));
3378                assert_matches!(signal_receiver.try_recv(), Err(_));
3379            }
3380        } else {
3381            if expected_tx_result.is_ok() {
3382                assert_matches!(result, Err(TransactionError::CommitCancelled));
3383            } else {
3384                assert_eq!(result, &expected_tx_result);
3385            }
3386
3387            assert_eq!(bank.transaction_count(), 0);
3388            assert_matches!(receiver.try_recv(), Err(_));
3389            assert_matches!(signal_receiver.try_recv(), Err(_));
3390        }
3391
3392        exit.store(true, Ordering::Relaxed);
3393        poh_service.join().unwrap();
3394    }
3395}