1
0
forked from ROMEO/nexosim

Make execution failible, impl deadlock detection

TODO: return the list of models involved in a deadlock.

Note that Many execution errors are not implemented at all at the
moment and will need separate PRs, namely:
- Terminated
- ModelError
- Panic
This commit is contained in:
Serge Barral
2024-10-20 12:35:44 +02:00
parent e7889c8e9b
commit 1cfaa00f9e
22 changed files with 556 additions and 223 deletions

View File

@ -113,14 +113,15 @@
//! # impl Model for ModelB {};
//! # let modelA_addr = Mailbox::<ModelA>::new().address();
//! # let modelB_addr = Mailbox::<ModelB>::new().address();
//! # let mut simu = SimInit::new().init(MonotonicTime::EPOCH);
//! # let mut simu = SimInit::new().init(MonotonicTime::EPOCH)?;
//! simu.process_event(
//! |m: &mut ModelA| {
//! m.output.connect(ModelB::input, modelB_addr);
//! },
//! (),
//! &modelA_addr
//! );
//! )?;
//! # Ok::<(), asynchronix::simulation::SimulationError>(())
//! ```
mod mailbox;
mod scheduler;
@ -143,7 +144,7 @@ use std::time::Duration;
use recycle_box::{coerce_box, RecycleBox};
use crate::executor::Executor;
use crate::executor::{Executor, ExecutorError};
use crate::model::{Context, Model, SetupContext};
use crate::ports::{InputFn, ReplierFn};
use crate::time::{AtomicTime, Clock, MonotonicTime};
@ -223,8 +224,8 @@ impl Simulation {
/// [`Clock::synchronize()`](crate::time::Clock::synchronize) on the configured
/// simulation clock. This method blocks until all newly processed events
/// have completed.
pub fn step(&mut self) {
self.step_to_next_bounded(MonotonicTime::MAX);
pub fn step(&mut self) -> Result<(), ExecutionError> {
self.step_to_next_bounded(MonotonicTime::MAX).map(|_| ())
}
/// Iteratively advances the simulation time by the specified duration, as
@ -234,10 +235,10 @@ impl Simulation {
/// time have completed. The simulation time upon completion is equal to the
/// initial simulation time incremented by the specified duration, whether
/// or not an event was scheduled for that time.
pub fn step_by(&mut self, duration: Duration) {
pub fn step_by(&mut self, duration: Duration) -> Result<(), ExecutionError> {
let target_time = self.time.read() + duration;
self.step_until_unchecked(target_time);
self.step_until_unchecked(target_time)
}
/// Iteratively advances the simulation time until the specified deadline,
@ -247,16 +248,14 @@ impl Simulation {
/// time have completed. The simulation time upon completion is equal to the
/// specified target time, whether or not an event was scheduled for that
/// time.
pub fn step_until(&mut self, target_time: MonotonicTime) -> Result<(), SchedulingError> {
pub fn step_until(&mut self, target_time: MonotonicTime) -> Result<(), ExecutionError> {
if self.time.read() >= target_time {
return Err(SchedulingError::InvalidScheduledTime);
return Err(ExecutionError::InvalidTargetTime(target_time));
}
self.step_until_unchecked(target_time);
Ok(())
self.step_until_unchecked(target_time)
}
/// Returns a scheduler handle.
/// Returns an owned scheduler handle.
pub fn scheduler(&self) -> Scheduler {
Scheduler::new(self.scheduler_queue.clone(), self.time.reader())
}
@ -265,15 +264,20 @@ impl Simulation {
///
/// Simulation time remains unchanged. The periodicity of the action, if
/// any, is ignored.
pub fn process(&mut self, action: Action) {
pub fn process(&mut self, action: Action) -> Result<(), ExecutionError> {
action.spawn_and_forget(&self.executor);
self.executor.run();
self.run()
}
/// Processes an event immediately, blocking until completion.
///
/// Simulation time remains unchanged.
pub fn process_event<M, F, T, S>(&mut self, func: F, arg: T, address: impl Into<Address<M>>)
pub fn process_event<M, F, T, S>(
&mut self,
func: F,
arg: T,
address: impl Into<Address<M>>,
) -> Result<(), ExecutionError>
where
M: Model,
F: for<'a> InputFn<'a, M, T, S>,
@ -297,18 +301,19 @@ impl Simulation {
};
self.executor.spawn_and_forget(fut);
self.executor.run();
self.run()
}
/// Processes a query immediately, blocking until completion.
///
/// Simulation time remains unchanged.
/// Simulation time remains unchanged. If the targeted model was not added
/// to the simulation, an `ExecutionError::InvalidQuery` is returned.
pub fn process_query<M, F, T, R, S>(
&mut self,
func: F,
arg: T,
address: impl Into<Address<M>>,
) -> Result<R, QueryError>
) -> Result<R, ExecutionError>
where
M: Model,
F: for<'a> ReplierFn<'a, M, T, R, S>,
@ -338,9 +343,17 @@ impl Simulation {
};
self.executor.spawn_and_forget(fut);
self.executor.run();
self.run()?;
reply_reader.try_read().map_err(|_| QueryError {})
reply_reader
.try_read()
.map_err(|_| ExecutionError::BadQuery)
}
fn run(&mut self) -> Result<(), ExecutionError> {
self.executor.run().map_err(|e| match e {
ExecutorError::Deadlock => ExecutionError::Deadlock(Vec::new()),
})
}
/// Advances simulation time to that of the next scheduled action if its
@ -349,7 +362,10 @@ impl Simulation {
///
/// If at least one action was found that satisfied the time bound, the
/// corresponding new simulation time is returned.
fn step_to_next_bounded(&mut self, upper_time_bound: MonotonicTime) -> Option<MonotonicTime> {
fn step_to_next_bounded(
&mut self,
upper_time_bound: MonotonicTime,
) -> Result<Option<MonotonicTime>, ExecutionError> {
// Function pulling the next action. If the action is periodic, it is
// immediately re-scheduled.
fn pull_next_action(scheduler_queue: &mut MutexGuard<SchedulerQueue>) -> Action {
@ -380,7 +396,10 @@ impl Simulation {
// Move to the next scheduled time.
let mut scheduler_queue = self.scheduler_queue.lock().unwrap();
let mut current_key = peek_next_key(&mut scheduler_queue)?;
let mut current_key = match peek_next_key(&mut scheduler_queue) {
Some(key) => key,
None => return Ok(None),
};
self.time.write(current_key.0);
loop {
@ -420,9 +439,9 @@ impl Simulation {
let current_time = current_key.0;
// TODO: check synchronization status?
self.clock.synchronize(current_time);
self.executor.run();
self.run()?;
return Some(current_time);
return Ok(Some(current_time));
}
};
}
@ -437,18 +456,19 @@ impl Simulation {
///
/// This method does not check whether the specified time lies in the future
/// of the current simulation time.
fn step_until_unchecked(&mut self, target_time: MonotonicTime) {
fn step_until_unchecked(&mut self, target_time: MonotonicTime) -> Result<(), ExecutionError> {
loop {
match self.step_to_next_bounded(target_time) {
// The target time was reached exactly.
Some(t) if t == target_time => return,
Ok(Some(t)) if t == target_time => return Ok(()),
// No actions are scheduled before or at the target time.
None => {
Ok(None) => {
// Update the simulation time.
self.time.write(target_time);
self.clock.synchronize(target_time);
return;
return Ok(());
}
Err(e) => return Err(e),
// The target time was not reached yet.
_ => {}
}
@ -479,6 +499,141 @@ impl fmt::Display for QueryError {
impl Error for QueryError {}
/// Information regarding a deadlocked model.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DeadlockInfo {
model_name: String,
mailbox_size: usize,
}
/// An error returned upon simulation execution failure.
///
/// Note that if a `Deadlock`, `ModelError` or `ModelPanic` is returned, any
/// subsequent attempt to run the simulation will return `Terminated`.
#[derive(Debug)]
pub enum ExecutionError {
/// The simulation has deadlocked.
///
/// Enlists all models with non-empty mailboxes.
Deadlock(Vec<DeadlockInfo>),
/// A model has aborted the simulation.
ModelError {
/// Name of the model.
model_name: String,
/// Error registered by the model.
error: Box<dyn Error>,
},
/// A panic was caught during execution with the message contained in the
/// payload.
Panic(String),
/// The specified target simulation time is in the past of the current
/// simulation time.
InvalidTargetTime(MonotonicTime),
/// The query was invalid and did not obtain a response.
BadQuery,
/// The simulation has been terminated due to an earlier deadlock, model
/// error or model panic.
Terminated,
}
impl fmt::Display for ExecutionError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Deadlock(list) => {
f.write_str(
"a simulation deadlock has been detected that involves the following models: ",
)?;
let mut first_item = true;
for info in list {
if first_item {
first_item = false;
} else {
f.write_str(", ")?;
}
write!(
f,
"'{}' ({} item{} in mailbox)",
info.model_name,
info.mailbox_size,
if info.mailbox_size == 1 { "" } else { "s" }
)?;
}
Ok(())
}
Self::ModelError { model_name, error } => {
write!(
f,
"the simulation has been aborted by model '{}' with the following error: {}",
model_name, error
)
}
Self::Panic(msg) => {
f.write_str("a panic has been caught during simulation:\n")?;
f.write_str(msg)
}
Self::InvalidTargetTime(time) => {
write!(
f,
"target simulation stamp {} lies in the past of the current simulation time",
time
)
}
Self::BadQuery => f.write_str("the query did not return any response; maybe the target model was not added to the simulation?"),
Self::Terminated => f.write_str("the simulation has been terminated"),
}
}
}
impl Error for ExecutionError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
if let Self::ModelError { error, .. } = &self {
Some(error.as_ref())
} else {
None
}
}
}
/// An error returned upon simulation execution or scheduling failure.
#[derive(Debug)]
pub enum SimulationError {
/// The execution of the simulation failed.
ExecutionError(ExecutionError),
/// An attempt to schedule an item failed.
SchedulingError(SchedulingError),
}
impl fmt::Display for SimulationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::ExecutionError(e) => e.fmt(f),
Self::SchedulingError(e) => e.fmt(f),
}
}
}
impl Error for SimulationError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::ExecutionError(e) => e.source(),
Self::SchedulingError(e) => e.source(),
}
}
}
impl From<ExecutionError> for SimulationError {
fn from(e: ExecutionError) -> Self {
Self::ExecutionError(e)
}
}
impl From<SchedulingError> for SimulationError {
fn from(e: SchedulingError) -> Self {
Self::SchedulingError(e)
}
}
/// Adds a model and its mailbox to the simulation bench.
pub(crate) fn add_model<M: Model>(
mut model: M,