solana/runtime/src/installed_scheduler_pool.rs

564 lines
20 KiB
Rust

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