633 lines
18 KiB
Rust
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);
|
|
}
|
|
}
|