mirror of
https://github.com/logos-blockchain/logos-blockchain-testing.git
synced 2026-01-02 13:23:13 +00:00
353 lines
9.7 KiB
Rust
353 lines
9.7 KiB
Rust
use std::{num::NonZeroUsize, sync::Arc, time::Duration};
|
|
|
|
use tracing::{debug, info};
|
|
|
|
use super::{
|
|
NodeControlCapability, expectation::Expectation, runtime::context::RunMetrics,
|
|
workload::Workload,
|
|
};
|
|
use crate::topology::{
|
|
config::{TopologyBuilder, TopologyConfig},
|
|
configs::{network::Libp2pNetworkLayout, wallet::WalletConfig},
|
|
generation::GeneratedTopology,
|
|
};
|
|
|
|
const DEFAULT_FUNDS_PER_WALLET: u64 = 100;
|
|
const MIN_EXPECTATION_BLOCKS: u32 = 2;
|
|
const MIN_EXPECTATION_FALLBACK_SECS: u64 = 10;
|
|
|
|
/// Immutable scenario definition shared between the runner, workloads, and
|
|
/// expectations.
|
|
pub struct Scenario<Caps = ()> {
|
|
topology: GeneratedTopology,
|
|
workloads: Vec<Arc<dyn Workload>>,
|
|
expectations: Vec<Box<dyn Expectation>>,
|
|
duration: Duration,
|
|
capabilities: Caps,
|
|
}
|
|
|
|
impl<Caps> Scenario<Caps> {
|
|
fn new(
|
|
topology: GeneratedTopology,
|
|
workloads: Vec<Arc<dyn Workload>>,
|
|
expectations: Vec<Box<dyn Expectation>>,
|
|
duration: Duration,
|
|
capabilities: Caps,
|
|
) -> Self {
|
|
Self {
|
|
topology,
|
|
workloads,
|
|
expectations,
|
|
duration,
|
|
capabilities,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn topology(&self) -> &GeneratedTopology {
|
|
&self.topology
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn workloads(&self) -> &[Arc<dyn Workload>] {
|
|
&self.workloads
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn expectations(&self) -> &[Box<dyn Expectation>] {
|
|
&self.expectations
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn expectations_mut(&mut self) -> &mut [Box<dyn Expectation>] {
|
|
&mut self.expectations
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn duration(&self) -> Duration {
|
|
self.duration
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn capabilities(&self) -> &Caps {
|
|
&self.capabilities
|
|
}
|
|
}
|
|
|
|
/// Builder used by callers to describe the desired scenario.
|
|
pub struct Builder<Caps = ()> {
|
|
topology: TopologyBuilder,
|
|
workloads: Vec<Arc<dyn Workload>>,
|
|
expectations: Vec<Box<dyn Expectation>>,
|
|
duration: Duration,
|
|
capabilities: Caps,
|
|
}
|
|
|
|
pub type ScenarioBuilder = Builder<()>;
|
|
|
|
/// Builder for shaping the scenario topology.
|
|
pub struct TopologyConfigurator<Caps> {
|
|
builder: Builder<Caps>,
|
|
validators: usize,
|
|
executors: usize,
|
|
network_star: bool,
|
|
}
|
|
|
|
impl<Caps: Default> Builder<Caps> {
|
|
#[must_use]
|
|
/// Start a builder from a topology description.
|
|
pub fn new(topology: TopologyBuilder) -> Self {
|
|
Self {
|
|
topology,
|
|
workloads: Vec::new(),
|
|
expectations: Vec::new(),
|
|
duration: Duration::ZERO,
|
|
capabilities: Caps::default(),
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_node_counts(validators: usize, executors: usize) -> Self {
|
|
Self::new(TopologyBuilder::new(TopologyConfig::with_node_numbers(
|
|
validators, executors,
|
|
)))
|
|
}
|
|
|
|
/// Convenience constructor that immediately enters topology configuration,
|
|
/// letting callers set counts via `validators`/`executors`.
|
|
pub fn topology() -> TopologyConfigurator<Caps> {
|
|
TopologyConfigurator::new(Self::new(TopologyBuilder::new(TopologyConfig::empty())))
|
|
}
|
|
|
|
/// Configure topology via a closure and return the scenario builder.
|
|
#[must_use]
|
|
pub fn topology_with(
|
|
f: impl FnOnce(TopologyConfigurator<Caps>) -> TopologyConfigurator<Caps>,
|
|
) -> Builder<Caps> {
|
|
let configurator = Self::topology();
|
|
f(configurator).apply()
|
|
}
|
|
}
|
|
|
|
impl<Caps> Builder<Caps> {
|
|
#[must_use]
|
|
/// Swap capabilities type carried with the scenario.
|
|
pub fn with_capabilities<NewCaps>(self, capabilities: NewCaps) -> Builder<NewCaps> {
|
|
let Self {
|
|
topology,
|
|
workloads,
|
|
expectations,
|
|
duration,
|
|
..
|
|
} = self;
|
|
|
|
Builder {
|
|
topology,
|
|
workloads,
|
|
expectations,
|
|
duration,
|
|
capabilities,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn capabilities(&self) -> &Caps {
|
|
&self.capabilities
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn capabilities_mut(&mut self) -> &mut Caps {
|
|
&mut self.capabilities
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_workload<W>(mut self, workload: W) -> Self
|
|
where
|
|
W: Workload + 'static,
|
|
{
|
|
self.expectations.extend(workload.expectations());
|
|
self.workloads.push(Arc::new(workload));
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
/// Add a standalone expectation not tied to a workload.
|
|
pub fn with_expectation<E>(mut self, expectation: E) -> Self
|
|
where
|
|
E: Expectation + 'static,
|
|
{
|
|
self.expectations.push(Box::new(expectation));
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
/// Configure the intended run duration.
|
|
pub const fn with_run_duration(mut self, duration: Duration) -> Self {
|
|
self.duration = duration;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
/// Transform the topology builder.
|
|
pub fn map_topology(mut self, f: impl FnOnce(TopologyBuilder) -> TopologyBuilder) -> Self {
|
|
self.topology = f(self.topology);
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
/// Override wallet config for the topology.
|
|
pub fn with_wallet_config(mut self, wallet: WalletConfig) -> Self {
|
|
self.topology = self.topology.with_wallet_config(wallet);
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn wallets(self, users: usize) -> Self {
|
|
let user_count = NonZeroUsize::new(users).expect("wallet user count must be non-zero");
|
|
let total_funds = DEFAULT_FUNDS_PER_WALLET
|
|
.checked_mul(users as u64)
|
|
.expect("wallet count exceeds capacity");
|
|
let wallet = WalletConfig::uniform(total_funds, user_count);
|
|
self.with_wallet_config(wallet)
|
|
}
|
|
|
|
#[must_use]
|
|
/// Finalize the scenario, computing run metrics and initializing
|
|
/// components.
|
|
pub fn build(self) -> Scenario<Caps> {
|
|
let Self {
|
|
topology,
|
|
mut workloads,
|
|
mut expectations,
|
|
duration,
|
|
capabilities,
|
|
..
|
|
} = self;
|
|
|
|
let generated = topology.build();
|
|
let duration = enforce_min_duration(&generated, duration);
|
|
let run_metrics = RunMetrics::from_topology(&generated, duration);
|
|
initialize_components(&generated, &run_metrics, &mut workloads, &mut expectations);
|
|
|
|
info!(
|
|
validators = generated.validators().len(),
|
|
executors = generated.executors().len(),
|
|
duration_secs = duration.as_secs(),
|
|
workloads = workloads.len(),
|
|
expectations = expectations.len(),
|
|
"scenario built"
|
|
);
|
|
|
|
Scenario::new(generated, workloads, expectations, duration, capabilities)
|
|
}
|
|
}
|
|
|
|
impl<Caps> TopologyConfigurator<Caps> {
|
|
const fn new(builder: Builder<Caps>) -> Self {
|
|
Self {
|
|
builder,
|
|
validators: 0,
|
|
executors: 0,
|
|
network_star: false,
|
|
}
|
|
}
|
|
|
|
/// Set the number of validator nodes.
|
|
#[must_use]
|
|
pub fn validators(mut self, count: usize) -> Self {
|
|
self.validators = count;
|
|
self
|
|
}
|
|
|
|
/// Set the number of executor nodes.
|
|
#[must_use]
|
|
pub fn executors(mut self, count: usize) -> Self {
|
|
self.executors = count;
|
|
self
|
|
}
|
|
|
|
/// Use a star libp2p network layout.
|
|
#[must_use]
|
|
pub fn network_star(mut self) -> Self {
|
|
self.network_star = true;
|
|
self
|
|
}
|
|
|
|
/// Finalize and return the underlying scenario builder.
|
|
#[must_use]
|
|
pub fn apply(self) -> Builder<Caps> {
|
|
let participants = self.validators + self.executors;
|
|
assert!(
|
|
participants > 0,
|
|
"topology must include at least one node; call validators()/executors() before apply()"
|
|
);
|
|
|
|
let mut config = TopologyConfig::with_node_numbers(self.validators, self.executors);
|
|
if self.network_star {
|
|
config.network_params.libp2p_network_layout = Libp2pNetworkLayout::Star;
|
|
}
|
|
|
|
let mut builder = self.builder;
|
|
builder.topology = TopologyBuilder::new(config);
|
|
builder
|
|
}
|
|
}
|
|
|
|
impl Builder<()> {
|
|
#[must_use]
|
|
pub fn enable_node_control(self) -> Builder<NodeControlCapability> {
|
|
self.with_capabilities(NodeControlCapability)
|
|
}
|
|
}
|
|
|
|
fn initialize_components(
|
|
descriptors: &GeneratedTopology,
|
|
run_metrics: &RunMetrics,
|
|
workloads: &mut [Arc<dyn Workload>],
|
|
expectations: &mut [Box<dyn Expectation>],
|
|
) {
|
|
initialize_workloads(descriptors, run_metrics, workloads);
|
|
initialize_expectations(descriptors, run_metrics, expectations);
|
|
}
|
|
|
|
fn initialize_workloads(
|
|
descriptors: &GeneratedTopology,
|
|
run_metrics: &RunMetrics,
|
|
workloads: &mut [Arc<dyn Workload>],
|
|
) {
|
|
for workload in workloads {
|
|
let inner =
|
|
Arc::get_mut(workload).expect("workload unexpectedly cloned before initialization");
|
|
|
|
debug!(workload = inner.name(), "initializing workload");
|
|
if let Err(err) = inner.init(descriptors, run_metrics) {
|
|
panic!("workload '{}' failed to initialize: {err}", inner.name());
|
|
}
|
|
}
|
|
}
|
|
|
|
fn initialize_expectations(
|
|
descriptors: &GeneratedTopology,
|
|
run_metrics: &RunMetrics,
|
|
expectations: &mut [Box<dyn Expectation>],
|
|
) {
|
|
for expectation in expectations {
|
|
debug!(expectation = expectation.name(), "initializing expectation");
|
|
if let Err(err) = expectation.init(descriptors, run_metrics) {
|
|
panic!(
|
|
"expectation '{}' failed to initialize: {err}",
|
|
expectation.name()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn enforce_min_duration(descriptors: &GeneratedTopology, requested: Duration) -> Duration {
|
|
let min_duration = descriptors.slot_duration().map_or_else(
|
|
|| Duration::from_secs(MIN_EXPECTATION_FALLBACK_SECS),
|
|
|slot| slot * MIN_EXPECTATION_BLOCKS,
|
|
);
|
|
|
|
requested.max(min_duration)
|
|
}
|