use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use crate::bytecode::{Chunk, Value};
use crate::vm::{NativeTable, Vm};
use crossbeam_deque::{Injector, Stealer, Worker as LocalDeque};
use super::directory::Directory;
use super::error::SpawnError;
use super::handle::FlowHandle;
use super::mailbox::{Delivery, Mailbox};
use super::metrics::{RuntimeMetrics, RuntimeMetricsSnapshot};
use super::process::{Flow, FlowId, RestartPolicy};
use super::supervisor::SupervisorLink;
use super::timer::TimerWheel;
use super::worker;
pub const DEFAULT_QUANTUM: u32 = 10_000;
#[derive(Clone, Debug)]
pub struct RuntimeConfig {
pub workers: usize,
pub quantum: u32,
}
impl Default for RuntimeConfig {
fn default() -> Self {
RuntimeConfig { workers: num_cpus::get().max(1), quantum: DEFAULT_QUANTUM }
}
}
pub struct Shared {
pub(crate) injector: Injector<Box<Flow>>,
pub(crate) stealers: Vec<Stealer<Box<Flow>>>,
pub(crate) directory: Directory,
pub(crate) caps: super::capability::CapTable,
pub(crate) timer: Arc<TimerWheel>,
pub(crate) notify: (Mutex<()>, Condvar),
pub(crate) metrics: RuntimeMetrics,
pub(crate) shutdown: AtomicBool,
pub(crate) quantum: u32,
}
pub struct Runtime {
shared: Arc<Shared>,
chunk: Arc<Chunk>,
natives: Arc<NativeTable>,
workers: Vec<JoinHandle<()>>,
timer_thread: Option<JoinHandle<()>>,
}
impl Runtime {
pub fn new(chunk: Chunk) -> Result<Self, SpawnError> {
Self::with_config(chunk, RuntimeConfig::default())
}
pub fn with_natives(chunk: Chunk, natives: Arc<NativeTable>) -> Result<Self, SpawnError> {
Self::with_natives_and_config(chunk, natives, RuntimeConfig::default())
}
pub fn with_config(chunk: Chunk, config: RuntimeConfig) -> Result<Self, SpawnError> {
Self::with_natives_and_config(chunk, NativeTable::empty(), config)
}
pub fn with_natives_and_config(
chunk: Chunk,
natives: Arc<NativeTable>,
config: RuntimeConfig,
) -> Result<Self, SpawnError> {
crate::bytecode::verify(&chunk).map_err(|e| SpawnError::VerifyFailed(e.to_string()))?;
let chunk = Arc::new(chunk);
let workers_n = config.workers.max(1);
let locals: Vec<LocalDeque<Box<Flow>>> =
(0..workers_n).map(|_| LocalDeque::new_fifo()).collect();
let stealers: Vec<Stealer<Box<Flow>>> = locals.iter().map(|l| l.stealer()).collect();
let shared = Arc::new(Shared {
injector: Injector::new(),
stealers,
directory: Directory::new(),
caps: super::capability::CapTable::new(),
timer: TimerWheel::new(),
notify: (Mutex::new(()), Condvar::new()),
metrics: RuntimeMetrics::default(),
shutdown: AtomicBool::new(false),
quantum: config.quantum,
});
let mut workers = Vec::with_capacity(workers_n);
for local in locals {
let shared = shared.clone();
let handle = std::thread::Builder::new()
.name("byteflow-worker".into())
.spawn(move || worker::run_worker(shared, local))
.map_err(|e| SpawnError::ThreadSpawnFailed(e.to_string()))?;
workers.push(handle);
}
let shared_timer = shared.clone();
let timer_thread = std::thread::Builder::new()
.name("byteflow-timer".into())
.spawn(move || {
shared_timer
.timer
.clone()
.drive(&shared_timer.injector, &shared_timer.notify)
})
.map_err(|e| SpawnError::ThreadSpawnFailed(e.to_string()))?;
Ok(Runtime {
shared,
chunk,
natives,
workers,
timer_thread: Some(timer_thread),
})
}
pub fn spawn(&self, function: u32, args: &[Value]) -> Result<FlowHandle, SpawnError> {
spawn_on(
&self.shared,
&self.chunk,
&self.natives,
function,
args,
RestartPolicy::Never,
None,
)
}
pub fn spawner(&self) -> RuntimeSpawner {
RuntimeSpawner { shared: self.shared.clone(), chunk: self.chunk.clone(), natives: self.natives.clone() }
}
pub fn supervisor(&self) -> Result<super::supervisor::Supervisor, SpawnError> {
super::supervisor::Supervisor::new(self.spawner())
}
pub fn function_index(&self, name: &str) -> Option<u32> {
self.chunk.functions.iter().position(|f| f.name == name).map(|i| i as u32)
}
pub fn metrics(&self) -> RuntimeMetricsSnapshot {
self.shared.metrics.snapshot()
}
pub fn live_flows(&self) -> usize {
self.shared.directory.len()
}
pub fn worker_count(&self) -> usize {
self.workers.len()
}
pub fn send(&self, target: FlowId, message: Value) -> Result<(), SendError> {
if message.as_message().is_none() {
return Err(SendError::NotAHop {
got: message.type_name(),
});
}
let mailbox = match self.shared.directory.lookup(target) {
Ok(Some(m)) => m,
Ok(None) => return Err(SendError::NoSuchFlow(target)),
Err(e) => {
super::error::report_fault(e);
return Err(SendError::NoSuchFlow(target));
}
};
match mailbox.push(message.clone()) {
Ok(Delivery::Queued) => Ok(()),
Ok(Delivery::Handoff(mut flow)) => {
if let Some(dest) = flow.last_receive_dest {
let _ = flow.vm.resume_with(dest, message);
}
self.shared.injector.push(flow);
wake_workers(&self.shared);
Ok(())
}
Err(e) => {
super::error::report_fault(e);
Err(SendError::NoSuchFlow(target))
}
}
}
pub fn shutdown(mut self) {
self.shared.shutdown.store(true, Ordering::Release);
self.shared.timer.shutdown();
{
let (lock, cvar) = &self.shared.notify;
match super::sync_lock::lock(lock, "Runtime::shutdown") {
Ok(_g) => cvar.notify_all(),
Err(e) => super::error::report_fault(e),
}
}
for w in self.workers.drain(..) {
let _ = w.join();
}
if let Some(t) = self.timer_thread.take() {
let _ = t.join();
}
}
}
pub fn flow_id_from_u64(raw: u64) -> FlowId {
FlowId(raw)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SendError {
NoSuchFlow(FlowId),
NotAHop { got: &'static str },
}
impl std::fmt::Display for SendError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SendError::NoSuchFlow(id) => write!(f, "no live flow {id}"),
SendError::NotAHop { got } => {
write!(f, "atomic hop requires Value::Message, got {got}")
}
}
}
}
impl std::error::Error for SendError {}
pub(crate) fn spawn_on(
shared: &Arc<Shared>,
chunk: &Arc<Chunk>,
natives: &Arc<NativeTable>,
function: u32,
args: &[Value],
restart_policy: RestartPolicy,
supervisor: Option<SupervisorLink>,
) -> Result<FlowHandle, SpawnError> {
let id = super::process::next_flow_id();
let vm = Vm::new(chunk.clone(), natives.clone(), function, args)?;
let mailbox = Arc::new(Mailbox::new());
if let Err(e) = shared.directory.register(id, mailbox.clone()) {
super::error::report_fault(e);
return Err(SpawnError::VmInit(
"directory register failed (poisoned lock)".into(),
));
}
let (tx, rx) = super::oneshot::channel();
let mut flow = Box::new(Flow::new(id, vm, mailbox, restart_policy, tx));
flow.supervisor = supervisor;
RuntimeMetrics::inc(&shared.metrics.processes_spawned);
shared.injector.push(flow);
wake_workers(shared);
Ok(FlowHandle { id, receiver: rx })
}
pub(crate) fn wake_workers(shared: &Shared) {
let (lock, cvar) = &shared.notify;
match super::sync_lock::lock(lock, "wake_workers") {
Ok(_g) => cvar.notify_one(),
Err(e) => super::error::report_fault(e),
}
}
#[derive(Clone)]
pub struct RuntimeSpawner {
pub(crate) shared: Arc<Shared>,
pub(crate) chunk: Arc<Chunk>,
pub(crate) natives: Arc<NativeTable>,
}
impl RuntimeSpawner {
pub fn spawn(
&self,
function: u32,
args: &[Value],
restart_policy: RestartPolicy,
) -> Result<FlowHandle, SpawnError> {
spawn_on(
&self.shared,
&self.chunk,
&self.natives,
function,
args,
restart_policy,
None,
)
}
pub(crate) fn spawn_linked(
&self,
function: u32,
args: &[Value],
restart_policy: RestartPolicy,
supervisor: SupervisorLink,
) -> Result<FlowHandle, SpawnError> {
spawn_on(
&self.shared,
&self.chunk,
&self.natives,
function,
args,
restart_policy,
Some(supervisor),
)
}
pub fn metrics(&self) -> RuntimeMetricsSnapshot {
self.shared.metrics.snapshot()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bytecode::{ChunkBuilder, Opcode, Value};
use crate::scheduler::FlowOutcome;
fn add_chunk() -> Chunk {
let mut b = ChunkBuilder::new("test");
b.begin_function("main", 0, 2);
b.emit_load_imm(0, 41);
b.emit_load_imm(1, 1);
b.emit_binop(Opcode::Add, 0, 0, 1);
b.emit_return(0);
b.finish()
}
#[test]
fn spawn_and_join_add() {
let rt = Runtime::with_config(
add_chunk(),
RuntimeConfig {
workers: 1,
quantum: 1_000,
},
)
.expect("runtime");
let outcome = rt.spawn(0, &[]).expect("spawn").join();
rt.shutdown();
match outcome {
FlowOutcome::Completed(Value::Int(42)) => {}
other => panic!("unexpected outcome: {other:?}"),
}
}
}