synor/crates/synor-vm/src/scheduler.rs
2026-02-02 05:58:22 +05:30

633 lines
18 KiB
Rust

//! Parallel Execution Scheduler (Sealevel-style).
//!
//! Enables parallel execution of transactions with non-conflicting state access.
//! Inspired by Solana's Sealevel runtime, adapted for Synor's DAG architecture.
//!
//! # How It Works
//!
//! 1. Transactions declare which accounts/storage they will read/write
//! 2. Scheduler groups transactions into batches with non-overlapping write sets
//! 3. Each batch executes in parallel across multiple threads
//! 4. Results are collected and state changes applied sequentially
//!
//! # Benefits
//!
//! - 10-100x throughput improvement for non-conflicting transactions
//! - Natural fit for DAG's parallel block production
//! - Maintains determinism through careful ordering
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use parking_lot::{Mutex, RwLock};
use crate::{ContractId, ExecutionResult, VmError};
/// Account/state access declaration for a transaction.
#[derive(Clone, Debug, Default)]
pub struct AccessSet {
/// Accounts/contracts that will be read.
pub reads: HashSet<ContractId>,
/// Accounts/contracts that will be written.
pub writes: HashSet<ContractId>,
}
impl AccessSet {
/// Creates a new empty access set.
pub fn new() -> Self {
Self::default()
}
/// Adds a read access.
pub fn add_read(&mut self, contract: ContractId) {
self.reads.insert(contract);
}
/// Adds a write access.
pub fn add_write(&mut self, contract: ContractId) {
// Writes implicitly include reads
self.reads.insert(contract);
self.writes.insert(contract);
}
/// Checks if two access sets conflict (can't execute in parallel).
pub fn conflicts_with(&self, other: &AccessSet) -> bool {
// Write-Write conflict
if !self.writes.is_disjoint(&other.writes) {
return true;
}
// Write-Read conflict
if !self.writes.is_disjoint(&other.reads) {
return true;
}
// Read-Write conflict
if !self.reads.is_disjoint(&other.writes) {
return true;
}
false
}
/// Merges another access set into this one.
pub fn merge(&mut self, other: &AccessSet) {
self.reads.extend(other.reads.iter().cloned());
self.writes.extend(other.writes.iter().cloned());
}
}
/// A transaction ready for parallel execution.
pub struct ScheduledTransaction<T> {
/// The transaction data.
pub tx: T,
/// Transaction index (for ordering).
pub index: usize,
/// Declared access set.
pub access: AccessSet,
}
/// Result of executing a scheduled transaction.
pub struct ScheduledResult<T> {
/// Original transaction.
pub tx: T,
/// Transaction index.
pub index: usize,
/// Execution result or error.
pub result: Result<ExecutionResult, VmError>,
}
/// A batch of transactions that can execute in parallel.
pub struct ExecutionBatch<T> {
/// Transactions in this batch.
pub transactions: Vec<ScheduledTransaction<T>>,
/// Combined access set for the batch.
pub combined_access: AccessSet,
}
impl<T> Default for ExecutionBatch<T> {
fn default() -> Self {
Self {
transactions: Vec::new(),
combined_access: AccessSet::default(),
}
}
}
impl<T> ExecutionBatch<T> {
/// Creates a new empty batch.
pub fn new() -> Self {
Self::default()
}
/// Tries to add a transaction to this batch.
/// Returns false if it conflicts with existing transactions.
pub fn try_add(&mut self, tx: ScheduledTransaction<T>) -> Result<(), ScheduledTransaction<T>> {
// Check for conflicts with current batch
if self.combined_access.conflicts_with(&tx.access) {
return Err(tx);
}
// Add to batch
self.combined_access.merge(&tx.access);
self.transactions.push(tx);
Ok(())
}
/// Returns the number of transactions in this batch.
pub fn len(&self) -> usize {
self.transactions.len()
}
/// Returns true if the batch is empty.
pub fn is_empty(&self) -> bool {
self.transactions.is_empty()
}
}
/// Statistics for parallel execution.
#[derive(Clone, Debug, Default)]
pub struct SchedulerStats {
/// Total transactions processed.
pub total_transactions: usize,
/// Number of batches created.
pub batch_count: usize,
/// Transactions that ran in parallel.
pub parallel_transactions: usize,
/// Transactions that had conflicts.
pub conflicting_transactions: usize,
/// Maximum batch size achieved.
pub max_batch_size: usize,
/// Average parallelism factor.
pub avg_parallelism: f64,
}
impl SchedulerStats {
/// Computes the parallelism improvement factor.
pub fn parallelism_factor(&self) -> f64 {
if self.batch_count == 0 {
return 1.0;
}
self.total_transactions as f64 / self.batch_count as f64
}
}
/// Configuration for the parallel scheduler.
#[derive(Clone, Debug)]
pub struct SchedulerConfig {
/// Maximum batch size.
pub max_batch_size: usize,
/// Number of worker threads.
pub worker_threads: usize,
/// Enable speculative execution.
pub enable_speculation: bool,
/// Maximum pending batches in queue.
pub queue_depth: usize,
}
impl Default for SchedulerConfig {
fn default() -> Self {
Self {
max_batch_size: 256,
worker_threads: num_cpus::get(),
enable_speculation: false,
queue_depth: 16,
}
}
}
/// The parallel execution scheduler.
pub struct ParallelScheduler<T> {
/// Configuration.
config: SchedulerConfig,
/// Current batches being built.
batches: Vec<ExecutionBatch<T>>,
/// Statistics.
stats: Mutex<SchedulerStats>,
}
impl<T> ParallelScheduler<T> {
/// Creates a new scheduler with default configuration.
pub fn new() -> Self {
Self::with_config(SchedulerConfig::default())
}
/// Creates a scheduler with custom configuration.
pub fn with_config(config: SchedulerConfig) -> Self {
Self {
config,
batches: Vec::new(),
stats: Mutex::new(SchedulerStats::default()),
}
}
/// Schedules transactions into parallel batches.
///
/// Uses a greedy algorithm:
/// 1. Try to add each transaction to the first batch it doesn't conflict with
/// 2. If no batch works, create a new batch
pub fn schedule(
&mut self,
transactions: Vec<ScheduledTransaction<T>>,
) -> Vec<ExecutionBatch<T>> {
let mut batches: Vec<ExecutionBatch<T>> = Vec::new();
let tx_count = transactions.len();
for tx in transactions {
// Find first non-conflicting batch
let mut current_tx = Some(tx);
for batch in batches.iter_mut() {
if batch.len() < self.config.max_batch_size {
if let Some(tx_to_add) = current_tx.take() {
match batch.try_add(tx_to_add) {
Ok(()) => {
// Successfully placed, current_tx is now None
break;
}
Err(returned_tx) => {
// Failed, put it back for next iteration
current_tx = Some(returned_tx);
}
}
}
}
}
// Create new batch if transaction wasn't placed
if let Some(tx_to_add) = current_tx {
let mut new_batch = ExecutionBatch::new();
// This should always succeed for a new batch
let _ = new_batch.try_add(tx_to_add);
batches.push(new_batch);
}
}
// Update statistics
{
let mut stats = self.stats.lock();
stats.total_transactions += tx_count;
stats.batch_count += batches.len();
let max_size = batches.iter().map(|b| b.len()).max().unwrap_or(0);
stats.max_batch_size = stats.max_batch_size.max(max_size);
if !batches.is_empty() {
stats.parallel_transactions += tx_count.saturating_sub(batches.len());
stats.avg_parallelism = stats.total_transactions as f64 / stats.batch_count as f64;
}
}
batches
}
/// Returns current statistics.
pub fn stats(&self) -> SchedulerStats {
self.stats.lock().clone()
}
/// Resets statistics.
pub fn reset_stats(&self) {
*self.stats.lock() = SchedulerStats::default();
}
}
impl<T> Default for ParallelScheduler<T> {
fn default() -> Self {
Self::new()
}
}
/// Executor trait for parallel execution.
pub trait ParallelExecutor<T> {
/// Executes a single transaction.
fn execute(&self, tx: &ScheduledTransaction<T>) -> Result<ExecutionResult, VmError>;
}
/// Executes batches in parallel using the provided executor.
pub async fn execute_batches_parallel<T, E>(
batches: Vec<ExecutionBatch<T>>,
executor: Arc<E>,
) -> Vec<ScheduledResult<T>>
where
T: Send + Sync + 'static + Clone,
E: ParallelExecutor<T> + Send + Sync + 'static,
{
let mut all_results: Vec<ScheduledResult<T>> = Vec::new();
// Execute batches sequentially (order matters for state)
// but transactions within each batch in parallel
for batch in batches {
let batch_results = execute_batch_parallel(batch, executor.clone()).await;
all_results.extend(batch_results);
}
// Sort by original index to maintain order
all_results.sort_by_key(|r| r.index);
all_results
}
/// Executes a single batch in parallel.
async fn execute_batch_parallel<T, E>(
batch: ExecutionBatch<T>,
executor: Arc<E>,
) -> Vec<ScheduledResult<T>>
where
T: Send + Sync + 'static + Clone,
E: ParallelExecutor<T> + Send + Sync + 'static,
{
let mut handles = Vec::new();
for tx in batch.transactions {
let executor = executor.clone();
let handle = tokio::spawn(async move {
let result = executor.execute(&tx);
ScheduledResult {
tx: tx.tx,
index: tx.index,
result,
}
});
handles.push(handle);
}
let mut results = Vec::new();
for handle in handles {
if let Ok(result) = handle.await {
results.push(result);
}
}
results
}
/// Builder for creating scheduled transactions with access declarations.
pub struct TransactionBuilder<T> {
tx: T,
index: usize,
access: AccessSet,
}
impl<T> TransactionBuilder<T> {
/// Creates a new builder.
pub fn new(tx: T, index: usize) -> Self {
Self {
tx,
index,
access: AccessSet::new(),
}
}
/// Declares a read access.
pub fn reads(mut self, contract: ContractId) -> Self {
self.access.add_read(contract);
self
}
/// Declares a write access.
pub fn writes(mut self, contract: ContractId) -> Self {
self.access.add_write(contract);
self
}
/// Builds the scheduled transaction.
pub fn build(self) -> ScheduledTransaction<T> {
ScheduledTransaction {
tx: self.tx,
index: self.index,
access: self.access,
}
}
}
/// Lock manager for ensuring safe parallel access.
pub struct LockManager {
/// Currently locked contracts.
locks: RwLock<HashMap<ContractId, LockState>>,
}
#[derive(Clone, Debug)]
enum LockState {
/// Read lock with count.
Read(usize),
/// Write lock.
Write,
}
impl LockManager {
/// Creates a new lock manager.
pub fn new() -> Self {
Self {
locks: RwLock::new(HashMap::new()),
}
}
/// Tries to acquire locks for an access set.
pub fn try_acquire(&self, access: &AccessSet) -> bool {
let mut locks = self.locks.write();
// Check all writes first
for contract in &access.writes {
if locks.get(contract).is_some() {
return false; // Any lock blocks write
}
}
// Check reads (only blocked by write locks)
for contract in &access.reads {
if !access.writes.contains(contract) {
if let Some(LockState::Write) = locks.get(contract) {
return false;
}
}
}
// Acquire all locks
for contract in &access.writes {
locks.insert(*contract, LockState::Write);
}
for contract in &access.reads {
if !access.writes.contains(contract) {
let entry = locks.entry(*contract).or_insert(LockState::Read(0));
if let LockState::Read(count) = entry {
*count += 1;
}
}
}
true
}
/// Releases locks for an access set.
pub fn release(&self, access: &AccessSet) {
let mut locks = self.locks.write();
for contract in &access.writes {
locks.remove(contract);
}
for contract in &access.reads {
if !access.writes.contains(contract) {
if let Some(LockState::Read(count)) = locks.get_mut(contract) {
*count -= 1;
if *count == 0 {
locks.remove(contract);
}
}
}
}
}
}
impl Default for LockManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_contract(id: u8) -> ContractId {
ContractId::from_bytes([id; 32])
}
#[test]
fn test_access_set_no_conflict() {
let mut a = AccessSet::new();
a.add_read(make_contract(1));
a.add_read(make_contract(2));
let mut b = AccessSet::new();
b.add_read(make_contract(1));
b.add_read(make_contract(3));
// Read-read is fine
assert!(!a.conflicts_with(&b));
}
#[test]
fn test_access_set_write_conflict() {
let mut a = AccessSet::new();
a.add_write(make_contract(1));
let mut b = AccessSet::new();
b.add_write(make_contract(1));
// Write-write conflicts
assert!(a.conflicts_with(&b));
}
#[test]
fn test_access_set_read_write_conflict() {
let mut a = AccessSet::new();
a.add_read(make_contract(1));
let mut b = AccessSet::new();
b.add_write(make_contract(1));
// Read-write conflicts
assert!(a.conflicts_with(&b));
}
#[test]
fn test_scheduler_parallel() {
// Three transactions that don't conflict
let tx1 = TransactionBuilder::new("tx1", 0)
.writes(make_contract(1))
.build();
let tx2 = TransactionBuilder::new("tx2", 1)
.writes(make_contract(2))
.build();
let tx3 = TransactionBuilder::new("tx3", 2)
.writes(make_contract(3))
.build();
let mut scheduler = ParallelScheduler::new();
let batches = scheduler.schedule(vec![tx1, tx2, tx3]);
// Should all be in one batch (no conflicts)
assert_eq!(batches.len(), 1);
assert_eq!(batches[0].len(), 3);
}
#[test]
fn test_scheduler_conflicts() {
// Three transactions that all conflict (write to same contract)
let tx1 = TransactionBuilder::new("tx1", 0)
.writes(make_contract(1))
.build();
let tx2 = TransactionBuilder::new("tx2", 1)
.writes(make_contract(1))
.build();
let tx3 = TransactionBuilder::new("tx3", 2)
.writes(make_contract(1))
.build();
let mut scheduler = ParallelScheduler::new();
let batches = scheduler.schedule(vec![tx1, tx2, tx3]);
// Each should be in its own batch (all conflict)
assert_eq!(batches.len(), 3);
}
#[test]
fn test_scheduler_mixed() {
// Mix of conflicting and non-conflicting
let tx1 = TransactionBuilder::new("tx1", 0)
.writes(make_contract(1))
.build();
let tx2 = TransactionBuilder::new("tx2", 1)
.writes(make_contract(2))
.build();
let tx3 = TransactionBuilder::new("tx3", 2)
.writes(make_contract(1))
.build();
let tx4 = TransactionBuilder::new("tx4", 3)
.writes(make_contract(3))
.build();
let mut scheduler = ParallelScheduler::new();
let batches = scheduler.schedule(vec![tx1, tx2, tx3, tx4]);
// tx1, tx2, tx4 can be parallel; tx3 conflicts with tx1
assert_eq!(batches.len(), 2);
}
#[test]
fn test_lock_manager() {
let manager = LockManager::new();
let mut access1 = AccessSet::new();
access1.add_write(make_contract(1));
let mut access2 = AccessSet::new();
access2.add_read(make_contract(1));
// First should succeed
assert!(manager.try_acquire(&access1));
// Second should fail (read blocked by write)
assert!(!manager.try_acquire(&access2));
// After release, should succeed
manager.release(&access1);
assert!(manager.try_acquire(&access2));
}
#[test]
fn test_scheduler_stats() {
let tx1 = TransactionBuilder::new("tx1", 0)
.writes(make_contract(1))
.build();
let tx2 = TransactionBuilder::new("tx2", 1)
.writes(make_contract(2))
.build();
let mut scheduler = ParallelScheduler::new();
scheduler.schedule(vec![tx1, tx2]);
let stats = scheduler.stats();
assert_eq!(stats.total_transactions, 2);
assert_eq!(stats.batch_count, 1);
assert!(stats.parallelism_factor() > 1.0);
}
}