Skip to main content

solana_unified_scheduler_pool/
lib.rs

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