From fa8b5cf034868c59aa39d17c55090c2d36fa45ad Mon Sep 17 00:00:00 2001 From: Serge Barral Date: Tue, 21 Jan 2025 02:11:23 +0100 Subject: [PATCH] Improve documentation, fix README example --- README.md | 35 ++- nexosim/Cargo.toml | 1 - nexosim/src/executor.rs | 8 +- nexosim/src/executor/mt_executor.rs | 8 +- nexosim/src/executor/st_executor.rs | 4 +- nexosim/src/lib.rs | 135 +++++---- nexosim/src/model.rs | 187 +++++------- nexosim/src/ports.rs | 271 +++++++++++++++--- nexosim/src/ports/input/model_fn.rs | 39 +-- nexosim/src/ports/sink.rs | 2 +- nexosim/src/ports/sink/event_buffer.rs | 3 +- nexosim/src/ports/sink/event_slot.rs | 3 +- nexosim/src/ports/source.rs | 10 +- nexosim/src/registry.rs | 6 +- nexosim/src/registry/event_sink_registry.rs | 2 +- nexosim/src/registry/event_source_registry.rs | 2 +- nexosim/src/registry/query_source_registry.rs | 4 +- .../src/server/services/controller_service.rs | 4 +- nexosim/src/simulation.rs | 58 ++-- nexosim/src/simulation/mailbox.rs | 7 +- nexosim/src/simulation/scheduler.rs | 17 +- nexosim/src/simulation/sim_init.rs | 2 +- nexosim/src/time.rs | 2 +- nexosim/src/time/clock.rs | 8 +- nexosim/src/tracing.rs | 2 +- 25 files changed, 495 insertions(+), 325 deletions(-) diff --git a/README.md b/README.md index 69fa31b..59fa8fd 100644 --- a/README.md +++ b/README.md @@ -90,20 +90,21 @@ asynchronix = "0.2.4" // Input ●─────►│ multiplier 1 ├─────►│ multiplier 2 ├─────► Output // │ │ │ │ // └──────────────┘ └──────────────┘ -use nexosim::model::{Model, Output}; -use nexosim::simulation::{Mailbox, SimInit}; -use nexosim::time::{MonotonicTime, Scheduler}; use std::time::Duration; +use nexosim::model::{Context, Model}; +use nexosim::ports::{EventSlot, Output}; +use nexosim::simulation::{Mailbox, SimInit}; +use nexosim::time::MonotonicTime; + // A model that doubles its input and forwards it with a 1s delay. #[derive(Default)] pub struct DelayedMultiplier { pub output: Output, } impl DelayedMultiplier { - pub fn input(&mut self, value: f64, scheduler: &Scheduler) { - scheduler - .schedule_event(Duration::from_secs(1), Self::send, 2.0 * value) + pub fn input(&mut self, value: f64, ctx: &mut Context) { + ctx.schedule_event(Duration::from_secs(1), Self::send, 2.0 * value) .unwrap(); } async fn send(&mut self, value: f64) { @@ -124,28 +125,32 @@ multiplier1 .connect(DelayedMultiplier::input, &multiplier2_mbox); // Keep handles to the main input and output. -let mut output_slot = multiplier2.output.connect_slot().0; +let mut output_slot = EventSlot::new(); +multiplier2.output.connect_sink(&output_slot); let input_address = multiplier1_mbox.address(); // Instantiate the simulator let t0 = MonotonicTime::EPOCH; // arbitrary start time let mut simu = SimInit::new() - .add_model(multiplier1, multiplier1_mbox) - .add_model(multiplier2, multiplier2_mbox) - .init(t0); + .add_model(multiplier1, multiplier1_mbox, "multiplier 1") + .add_model(multiplier2, multiplier2_mbox, "multiplier 2") + .init(t0)? + .0; // Send a value to the first multiplier. -simu.send_event(DelayedMultiplier::input, 3.5, &input_address); +simu.process_event(DelayedMultiplier::input, 3.5, &input_address)?; // Advance time to the next event. -simu.step(); +simu.step()?; assert_eq!(simu.time(), t0 + Duration::from_secs(1)); -assert_eq!(output_slot.take(), None); +assert_eq!(output_slot.next(), None); // Advance time to the next event. -simu.step(); +simu.step()?; assert_eq!(simu.time(), t0 + Duration::from_secs(2)); -assert_eq!(output_slot.take(), Some(14.0)); +assert_eq!(output_slot.next(), Some(14.0)); + +Ok(()) ``` # Implementation notes diff --git a/nexosim/Cargo.toml b/nexosim/Cargo.toml index daf1299..b0be993 100644 --- a/nexosim/Cargo.toml +++ b/nexosim/Cargo.toml @@ -29,7 +29,6 @@ keywords = [ ] [features] -# gRPC service. server = [ "dep:bytes", "dep:ciborium", diff --git a/nexosim/src/executor.rs b/nexosim/src/executor.rs index 2a7c99d..90f8884 100644 --- a/nexosim/src/executor.rs +++ b/nexosim/src/executor.rs @@ -79,8 +79,8 @@ impl Executor { /// Spawns a task which output will never be retrieved. /// - /// Note that spawned tasks are not executed until [`run()`](Executor::run) - /// is called. + /// Note that spawned tasks are not executed until [`run`](Executor::run) is + /// called. #[allow(unused)] pub(crate) fn spawn(&self, future: T) -> Promise where @@ -95,8 +95,8 @@ impl Executor { /// Spawns a task which output will never be retrieved. /// - /// Note that spawned tasks are not executed until [`run()`](Executor::run) - /// is called. + /// Note that spawned tasks are not executed until [`run`](Executor::run) is + /// called. pub(crate) fn spawn_and_forget(&self, future: T) where T: Future + Send + 'static, diff --git a/nexosim/src/executor/mt_executor.rs b/nexosim/src/executor/mt_executor.rs index 017830e..eb2732c 100644 --- a/nexosim/src/executor/mt_executor.rs +++ b/nexosim/src/executor/mt_executor.rs @@ -186,8 +186,8 @@ impl Executor { /// Spawns a task and returns a promise that can be polled to retrieve the /// task's output. /// - /// Note that spawned tasks are not executed until [`run()`](Executor::run) - /// is called. + /// Note that spawned tasks are not executed until [`run`](Executor::run) is + /// called. pub(crate) fn spawn(&self, future: T) -> Promise where T: Future + Send + 'static, @@ -215,8 +215,8 @@ impl Executor { /// This is mostly useful to avoid undue reference counting for futures that /// return a `()` type. /// - /// Note that spawned tasks are not executed until [`run()`](Executor::run) - /// is called. + /// Note that spawned tasks are not executed until [`run`](Executor::run) is + /// called. pub(crate) fn spawn_and_forget(&self, future: T) where T: Future + Send + 'static, diff --git a/nexosim/src/executor/st_executor.rs b/nexosim/src/executor/st_executor.rs index 6fd6bee..497b2a7 100644 --- a/nexosim/src/executor/st_executor.rs +++ b/nexosim/src/executor/st_executor.rs @@ -59,7 +59,7 @@ impl Executor { /// Spawns a task and returns a promise that can be polled to retrieve the /// task's output. /// - /// Note that spawned tasks are not executed until [`run()`](Executor::run) + /// Note that spawned tasks are not executed until [`run`](Executor::run) /// is called. pub(crate) fn spawn(&self, future: T) -> Promise where @@ -91,7 +91,7 @@ impl Executor { /// This is mostly useful to avoid undue reference counting for futures that /// return a `()` type. /// - /// Note that spawned tasks are not executed until [`run()`](Executor::run) + /// Note that spawned tasks are not executed until [`run`](Executor::run) /// is called. pub(crate) fn spawn_and_forget(&self, future: T) where diff --git a/nexosim/src/lib.rs b/nexosim/src/lib.rs index c627a48..06c01d6 100644 --- a/nexosim/src/lib.rs +++ b/nexosim/src/lib.rs @@ -39,9 +39,9 @@ //! * _output ports_, which are instances of the [`Output`](ports::Output) type //! and can be used to broadcast a message, //! * _requestor ports_, which are instances of the -//! [`Requestor`](ports::Requestor) type and can be used to broadcast a -//! message and receive an iterator yielding the replies from all connected -//! replier ports, +//! [`Requestor`](ports::Requestor) or [`UniRequestor`](ports::UniRequestor) +//! types and can be used to broadcast a message and receive an iterator +//! yielding the replies from all connected replier ports, //! * _input ports_, which are synchronous or asynchronous methods that //! implement the [`InputFn`](ports::InputFn) trait and take an `&mut self` //! argument, a message argument, and an optional @@ -54,19 +54,27 @@ //! are referred to as *requests* and *replies*. //! //! Models must implement the [`Model`](model::Model) trait. The main purpose of -//! this trait is to allow models to specify -//! * a `setup()` method that is called once during model addtion to simulation, -//! this method allows e.g. creation and interconnection of submodels inside -//! the model, -//! * an `init()` method that is guaranteed to run once and only once when the -//! simulation is initialized, _i.e._ after all models have been connected but -//! before the simulation starts. +//! this trait is to allow models to specify a +//! [`Model::init`](model::Model::init) method that is guaranteed to run once +//! and only once when the simulation is initialized, _i.e._ after all models +//! have been connected but before the simulation starts. //! -//! The `setup()` and `init()` methods have default implementations, so models -//! that do not require setup and initialization can simply implement the trait -//! with a one-liner such as `impl Model for MyModel {}`. +//! The [`Model::init`](model::Model::init) methods has a default +//! implementations, so models that do not require setup and initialization can +//! simply implement the trait with a one-liner such as `impl Model for MyModel +//! {}`. //! -//! #### A simple model +//! More complex models can be built with the [`ProtoModel`](model::ProtoModel) +//! trait. The [`ProtoModel::build`](model::ProtoModel::build) method makes it +//! possible to: +//! +//! * build the final [`Model`](model::Model) from a builder (the *model prototype*), +//! * perform possibly blocking actions when the model is added to the +//! simulation rather than when the simulation starts, such as establishing a +//! network connection or configuring hardware devices, +//! * connect submodels and add them to the simulation. +//! +//! ### A simple model //! //! Let us consider for illustration a simple model that forwards its input //! after multiplying it by 2. This model has only one input and one output @@ -98,7 +106,7 @@ //! impl Model for Multiplier {} //! ``` //! -//! #### A model using the local context +//! ### A model using the local context //! //! Models frequently need to schedule actions at a future time or simply get //! access to the current simulation time. To do so, input and replier methods @@ -141,7 +149,7 @@ //! [`Address`](simulation::Mailbox)es pointing to that mailbox. //! //! Addresses are used among others to connect models: each output or requestor -//! port has a `connect()` method that takes as argument a function pointer to +//! port has a `connect` method that takes as argument a function pointer to //! the corresponding input or replier port method and the address of the //! targeted model. //! @@ -230,7 +238,7 @@ //! //! // Pick an arbitrary simulation start time and build the simulation. //! let t0 = MonotonicTime::EPOCH; -//! let mut simu = SimInit::new() +//! let (mut simu, scheduler) = SimInit::new() //! .add_model(multiplier1, multiplier1_mbox, "multiplier1") //! .add_model(multiplier2, multiplier2_mbox, "multiplier2") //! .add_model(delay1, delay1_mbox, "delay1") @@ -245,27 +253,27 @@ //! The simulation can be controlled in several ways: //! //! 1. by advancing time, either until the next scheduled event with -//! [`Simulation::step()`](simulation::Simulation::step), or until a specific +//! [`Simulation::step`](simulation::Simulation::step), until a specific //! deadline with -//! [`Simulation::step_until()`](simulation::Simulation::step_until). +//! [`Simulation::step_until`](simulation::Simulation::step_until), or +//! until there are no more scheduled events with +//! [`Simulation::step_unbounded`](simulation::Simulation::step_unbounded). //! 2. by sending events or queries without advancing simulation time, using -//! [`Simulation::process_event()`](simulation::Simulation::process_event) or -//! [`Simulation::send_query()`](simulation::Simulation::process_query), -//! 3. by scheduling events, using for instance -//! [`Scheduler::schedule_event()`](simulation::Scheduler::schedule_event). +//! [`Simulation::process_event`](simulation::Simulation::process_event) or +//! [`Simulation::send_query`](simulation::Simulation::process_query), +//! 3. by scheduling events with a [`Scheduler`](simulation::Scheduler). //! //! When initialized with the default clock, the simulation will run as fast as //! possible, without regard for the actual wall clock time. Alternatively, the //! simulation time can be synchronized to the wall clock time using -//! [`SimInit::set_clock()`](simulation::SimInit::set_clock) and providing a +//! [`SimInit::set_clock`](simulation::SimInit::set_clock) and providing a //! custom [`Clock`](time::Clock) type or a readily-available real-time clock //! such as [`AutoSystemClock`](time::AutoSystemClock). //! -//! Simulation outputs can be monitored using [`EventSlot`](ports::EventSlot)s -//! and [`EventBuffer`](ports::EventBuffer)s, which can be connected to any -//! model's output port. While an event slot only gives access to the last value -//! sent from a port, an event stream is an iterator that yields all events that -//! were sent in first-in-first-out order. +//! Simulation outputs can be monitored using [`EventSlot`](ports::EventSlot)s, +//! [`EventBuffer`](ports::EventBuffer)s, or any implementer of the +//! [`EventSink`](ports::EventSink) trait, connected to one or several model +//! output ports. //! //! This is an example of simulation that could be performed using the above //! bench assembly: @@ -373,20 +381,20 @@ //! processed in any order by `B` and `C`, it is guaranteed that `B` will //! process `M1` before `M3`. //! -//! The first guarantee (and only the first) also extends to events scheduled -//! from a simulation with a -//! [`Scheduler::schedule_*()`](simulation::Scheduler::schedule_event) method: -//! if the scheduler contains several events to be delivered at the same time to -//! the same model, these events will always be processed in the order in which -//! they were scheduled. +//! Both guarantees also extend to same-time events scheduled from the global +//! [`Scheduler`](simulation::Scheduler), *i.e.* the relative ordering of events +//! scheduled for the same time is preserved and warranties 1 and 2 above +//! accordingly hold (assuming model `A` stands for the scheduler). Likewise, +//! the relative order of same-time events self-scheduled by a model using its +//! [`Context`](model::Context) is preserved. //! //! [actor_model]: https://en.wikipedia.org/wiki/Actor_model //! [pony]: https://www.ponylang.io/ //! //! -//! # Feature flags +//! # Cargo feature flags //! -//! ## Tracing support +//! ## Tracing //! //! The `tracing` feature flag provides support for the //! [`tracing`](https://docs.rs/tracing/latest/tracing/) crate and can be @@ -409,32 +417,50 @@ //! nexosim = { version = "0.3.0-beta.0", features = ["server"] } //! ``` //! +//! See the [`registry`] and [`server`] modules for more information. +//! +//! Front-end usage documentation will be added upon release of the NeXosim +//! Python client. +//! +//! //! # Other resources //! //! ## Other examples //! -//! The [`examples`][gh_examples] directory in the main repository contains more -//! fleshed out examples that demonstrate various capabilities of the simulation +//! Several [`examples`][gh_examples] are available that contain more fleshed +//! out examples and demonstrate various capabilities of the simulation //! framework. //! //! [gh_examples]: //! https://github.com/asynchronics/nexosim/tree/main/nexosim/examples //! -//! ## Modules documentation -//! -//! While the above overview does cover the basic concepts, more information is -//! available in the documentation of the different modules: -//! -//! * the [`model`] module provides more details about the signatures of input -//! and replier port methods and discusses model initialization in the -//! documentation of [`model::Model`] and self-scheduling methods as well as -//! scheduling cancellation in the documentation of [`model::Context`], -//! * the [`simulation`] module discusses how the capacity of mailboxes may -//! affect the simulation, how connections can be modified after the -//! simulation was instantiated, and which pathological situations can lead to -//! a deadlock, -//! * the [`time`] module discusses in particular the monotonic timestamp format -//! used for simulations ([`time::MonotonicTime`]). +//! +//! ## Other features and advanced topics +//! +//! While the above overview does cover most basic concepts, more information is +//! available in the modules' documentation: +//! +//! * the [`model`] module provides more details about models, **model +//! prototypes** and **hierarchical models**; be sure to check as well the +//! documentation of [`model::Context`] for topics such as **self-scheduling** +//! methods and **event cancellation**, +//! * the [`ports`] module discusses in more details model ports and simulation +//! endpoints, as well as the ability to **modify and filter messages** +//! exchanged between ports; it also provides +//! [`EventSource`](ports::EventSource) and +//! [`QuerySource`](ports::QuerySource) objects which can be connected to +//! models just like [`Output`](ports::Output) and +//! [`Requestor`](ports::Requestor) ports, but for use as simulation +//! endpoints. +//! * the [`registry`] and [`server`] modules make it possible to manage and +//! monitor a simulation locally or remotely from a NeXosim Python client, +//! * the [`simulation`] module discusses **mailbox capacity** and pathological +//! situations that may lead to a **deadlock**, +//! * the [`time`] module introduces the [`time::MonotonicTime`] monotonic +//! timestamp object and **simulation clocks**. +//! * the [`tracing`] module discusses time-stamping and filtering of `tracing` +//! events. +//! #![warn(missing_docs, missing_debug_implementations, unreachable_pub)] #![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg_hide))] #![cfg_attr(docsrs, doc(cfg_hide(feature = "dev-hooks")))] @@ -458,4 +484,5 @@ pub mod server; pub mod tracing; #[cfg(feature = "dev-hooks")] +#[doc(hidden)] pub mod dev_hooks; diff --git a/nexosim/src/model.rs b/nexosim/src/model.rs index 73969f8..82a534a 100644 --- a/nexosim/src/model.rs +++ b/nexosim/src/model.rs @@ -5,30 +5,30 @@ //! Every model must implement the [`Model`] trait. This trait defines an //! asynchronous initialization method, [`Model::init`], which main purpose is //! to enable models to perform specific actions when the simulation starts, -//! i.e. after all models have been connected and added to the simulation. +//! *i.e.* after all models have been connected and added to the simulation. //! //! It is frequently convenient to expose to users a model builder type—called a //! *model prototype*—rather than the final model. This can be done by //! implementing the [`ProtoModel`] trait, which defines the associated model //! type and a [`ProtoModel::build`] method invoked when a model is added the -//! the simulation and returning the actual model instance. +//! simulation. //! //! Prototype models can be used whenever the Rust builder pattern is helpful, //! for instance to set optional parameters. One of the use-cases that may //! benefit from the use of prototype models is hierarchical model building. //! When a parent model contains submodels, these submodels are often an //! implementation detail that needs not be exposed to the user. One may then -//! define a prototype model that contains all outputs and requestors ports. -//! Upon invocation of [`ProtoModel::build`], the ports are moved to the -//! appropriate submodels and those submodels are added to the simulation. +//! define a prototype model that contains all outputs and requestors ports, +//! while the model itself contains the input and replier ports. Upon invocation +//! of [`ProtoModel::build`], the exit ports are moved to the model or its +//! submodels, and those submodels are added to the simulation. //! //! Note that a trivial [`ProtoModel`] implementation is generated by default //! for any object implementing the [`Model`] trait, where the associated -//! [`ProtoModel::Model`] type is the model type itself and where -//! [`ProtoModel::build`] simply returns the model instance. This is what makes -//! it possible to use either an explicitly-defined [`ProtoModel`] as argument -//! to the [`SimInit::add_model`](crate::simulation::SimInit::add_model) method, -//! or a plain [`Model`] type. +//! [`ProtoModel::Model`] type is the model type itself. This is what makes it +//! possible to use either an explicitly-defined [`ProtoModel`] as argument to +//! the [`SimInit::add_model`](crate::simulation::SimInit::add_model) method, or +//! a plain [`Model`] type. //! //! #### Examples //! @@ -66,7 +66,9 @@ //! ``` //! //! Finally, if a model builder is required, the [`ProtoModel`] trait can be -//! explicitly implemented: +//! explicitly implemented. Note that the [`ProtoModel`] contains all output and +//! requestor ports, while the associated [`Model`] contains all input and +//! replier methods. //! //! ``` //! use nexosim::model::{BuildContext, InitializedModel, Model, ProtoModel}; @@ -117,120 +119,81 @@ //! } //! ``` //! +//! # Hierarchical models //! -//! # Events and queries +//! Hierarchical models are models build from a prototype, which prototype adds +//! submodels to the simulation within its [`ProtoModel::build`] method. From a +//! formal point of view, however, hierarchical models are just regular models +//! implementing the [`Model`] trait, as are their submodels. //! -//! Models can exchange data via *events* and *queries*. //! -//! Events are send-and-forget messages that can be broadcast from an *output -//! port* to an arbitrary number of *input ports* with a matching event type. -//! -//! Queries actually involve two messages: a *request* that can be broadcast -//! from a *requestor port* to an arbitrary number of *replier ports* with a -//! matching request type, and a *reply* sent in response to such request. The -//! response received by a requestor port is an iterator that yields as many -//! items (replies) as there are connected replier ports. -//! -//! -//! ### Output and requestor ports +//! #### Example //! -//! Output and requestor ports can be added to a model using composition, adding -//! [`Output`](crate::ports::Output) and [`Requestor`](crate::ports::Requestor) -//! objects as members. They are parametrized by the event, request and reply -//! types. +//! This example demonstrates a child model inside a parent model, where the +//! parent model simply forwards input data to the child and the child in turn +//! sends the data to the output exposed by the parent's prototype. //! -//! Models are expected to expose their output and requestor ports as public -//! members so they can be connected to input and replier ports when assembling -//! the simulation bench. +//! For a more comprehensive example demonstrating hierarchical model +//! assemblies, see the [`assembly`][assembly] example. //! -//! #### Example +//! [assembly]: +//! https://github.com/asynchronics/nexosim/tree/main/nexosim/examples/assembly.rs //! //! ``` -//! use nexosim::model::Model; -//! use nexosim::ports::{Output, Requestor}; +//! use nexosim::model::{BuildContext, Model, ProtoModel}; +//! use nexosim::ports::Output; +//! use nexosim::simulation::Mailbox; //! -//! pub struct MyModel { -//! pub my_output: Output, -//! pub my_requestor: Requestor, +//! pub struct ParentModel { +//! // Private internal port connected to the submodel. +//! to_child: Output, //! } -//! impl MyModel { -//! // ... +//! impl ParentModel { +//! async fn input(&mut self, my_data: u64) { +//! // Forward to the submodel. +//! self.to_child.send(my_data).await; +//! } //! } -//! impl Model for MyModel {} -//! ``` -//! -//! -//! ### Input and replier ports -//! -//! Input ports and replier ports are methods that implement the -//! [`InputFn`](crate::ports::InputFn) or [`ReplierFn`](crate::ports::ReplierFn) -//! traits with appropriate bounds on their argument and return types. -//! -//! In practice, an input port method for an event of type `T` may have any of -//! the following signatures, where the futures returned by the `async` variants -//! must implement `Send`: +//! impl Model for ParentModel {} //! -//! ```ignore -//! fn(&mut self) // argument elided, implies `T=()` -//! fn(&mut self, T) -//! fn(&mut self, T, &mut Context) -//! async fn(&mut self) // argument elided, implies `T=()` -//! async fn(&mut self, T) -//! async fn(&mut self, T, &mut Context) -//! where -//! Self: Model, -//! T: Clone + Send + 'static, -//! R: Send + 'static, -//! ``` +//! pub struct ProtoParentModel { +//! pub output: Output, +//! } +//! impl ProtoModel for ProtoParentModel { +//! type Model = ParentModel; //! -//! The context argument is useful for methods that need access to the -//! simulation time or that need to schedule an action at a future date. -//! -//! A replier port for a request of type `T` with a reply of type `R` may in -//! turn have any of the following signatures, where the futures must implement -//! `Send`: -//! -//! ```ignore -//! async fn(&mut self) -> R // argument elided, implies `T=()` -//! async fn(&mut self, T) -> R -//! async fn(&mut self, T, &mut Context) -> R -//! where -//! Self: Model, -//! T: Clone + Send + 'static, -//! R: Send + 'static, -//! ``` +//! fn build(self, cx: &mut BuildContext) -> ParentModel { +//! // Move the output to the child model. +//! let child = ChildModel { output: self.output }; +//! let mut parent = ParentModel { +//! to_child: Output::default(), +//! }; //! -//! Output and replier ports will normally be exposed as public methods so they -//! can be connected to input and requestor ports when assembling the simulation -//! bench. However, input ports may instead be defined as private methods if -//! they are only used by the model itself to schedule future actions (see the -//! [`Context`] examples). +//! let child_mailbox = Mailbox::new(); //! -//! Changing the signature of an input or replier port is not considered to -//! alter the public interface of a model provided that the event, request and -//! reply types remain the same. +//! // Establish an internal Parent -> Child connection. +//! parent +//! .to_child +//! .connect(ChildModel::input, child_mailbox.address()); //! -//! #### Example +//! // Add the child model to the simulation. +//! cx.add_submodel(child, child_mailbox, "child"); //! -//! ``` -//! use nexosim::model::{Context, Model}; +//! parent +//! } +//! } //! -//! pub struct MyModel { -//! // ... +//! struct ChildModel { +//! output: Output, //! } -//! impl MyModel { -//! pub fn my_input(&mut self, input: String, cx: &mut Context) { -//! // ... -//! } -//! pub async fn my_replier(&mut self, request: u32) -> bool { // context argument elided -//! // ... -//! # unimplemented!() +//! impl ChildModel { +//! async fn input(&mut self, my_data: u64) { +//! self.output.send(my_data).await; //! } //! } -//! impl Model for MyModel {} -//! ``` +//! impl Model for ChildModel {} //! - +//! ``` use std::future::Future; pub use context::{BuildContext, Context}; @@ -240,7 +203,7 @@ mod context; /// Trait to be implemented by simulation models. /// /// This trait enables models to perform specific actions during initialization. -/// The [`Model::init()`] method is run only once all models have been connected +/// The [`Model::init`] method is run only once all models have been connected /// and migrated to the simulation bench, but before the simulation actually /// starts. A common use for `init` is to send messages to connected models at /// the beginning of the simulation. @@ -253,7 +216,7 @@ pub trait Model: Sized + Send + 'static { /// /// This asynchronous method is executed exactly once for all models of the /// simulation when the - /// [`SimInit::init()`](crate::simulation::SimInit::init) method is called. + /// [`SimInit::init`](crate::simulation::SimInit::init) method is called. /// /// The default implementation simply converts the model to an /// `InitializedModel` without any side effect. @@ -290,7 +253,7 @@ pub trait Model: Sized + Send + 'static { /// /// A model can be converted to an `InitializedModel` using the `Into`/`From` /// traits. The implementation of the simulation guarantees that the -/// [`Model::init()`] method will never be called on a model after conversion to +/// [`Model::init`] method will never be called on a model after conversion to /// an `InitializedModel`. #[derive(Debug)] pub struct InitializedModel(pub(crate) M); @@ -301,18 +264,16 @@ impl From for InitializedModel { } } -/// Trait to be implemented by model prototypes. +/// Trait to be implemented by simulation model prototypes. /// /// This trait makes it possible to build the final model from a builder type /// when it is added to the simulation. /// -/// The [`ProtoModel::build()`] method consumes the prototype. It is +/// The [`ProtoModel::build`] method consumes the prototype. It is /// automatically called when a model or submodel prototype is added to the /// simulation using -/// [`Simulation::add_model()`](crate::simulation::SimInit::add_model) or +/// [`Simulation::add_model`](crate::simulation::SimInit::add_model) or /// [`BuildContext::add_submodel`]. -/// -/// The pub trait ProtoModel: Sized { /// Type of the model to be built. type Model: Model; @@ -320,8 +281,8 @@ pub trait ProtoModel: Sized { /// Builds the model. /// /// This method is invoked when the - /// [`SimInit::add_model()`](crate::simulation::SimInit::add_model) or - /// [`BuildContext::add_submodel`] method is called. + /// [`SimInit::add_model`](crate::simulation::SimInit::add_model) or + /// [`BuildContext::add_submodel`] method are called. fn build(self, cx: &mut BuildContext) -> Self::Model; } diff --git a/nexosim/src/ports.rs b/nexosim/src/ports.rs index f17be4c..c7b55f7 100644 --- a/nexosim/src/ports.rs +++ b/nexosim/src/ports.rs @@ -1,63 +1,182 @@ -//! Model ports for event and query broadcasting. +//! Ports for event and query broadcasting. //! -//! Models typically contain [`Output`] and/or [`Requestor`] ports, exposed as -//! public member variables. Output ports broadcast events to all connected -//! input ports, while requestor ports broadcast queries to, and retrieve -//! replies from, all connected replier ports. +//! +//! # Events and queries +//! +//! Models can exchange data via *events* and *queries*. +//! +//! Events are send-and-forget messages that can be broadcast from an [`Output`] +//! port or [`EventSource`] to an arbitrary number of *input ports* or +//! [`EventSink`]s with a matching event type. +//! +//! Queries actually involve two messages: a *request* that can be broadcast +//! from a [`Requestor`] port, a [`UniRequestor`] port or a [`QuerySource`] to +//! an arbitrary number of *replier ports* with a matching request type, and a +//! *reply* sent in response to such request. The response received by a +//! [`Requestor`] port is an iterator that yields as many items (replies) as +//! there are connected replier ports, while a [`UniRequestor`] received exactly +//! one reply. +//! +//! # Model ports +//! +//! ## Input and replier ports +//! +//! Input ports and replier ports are methods that implement the [`InputFn`] or +//! [`ReplierFn`] traits. +//! +//! In practice, an input port method for an event of type `T` may have any of +//! the following signatures, where the futures returned by the `async` variants +//! must implement `Send`: +//! +//! ```ignore +//! fn(&mut self) // argument elided, implies `T=()` +//! fn(&mut self, T) +//! fn(&mut self, T, &mut Context) +//! async fn(&mut self) // argument elided, implies `T=()` +//! async fn(&mut self, T) +//! async fn(&mut self, T, &mut Context) +//! where +//! Self: Model, +//! T: Clone + Send + 'static, +//! R: Send + 'static, +//! ``` +//! +//! The context argument is useful for methods that need access to the +//! simulation time or that need to schedule an action at a future date. +//! +//! A replier port for a request of type `T` with a reply of type `R` may in +//! turn have any of the following signatures, where the futures must implement +//! `Send`: +//! +//! ```ignore +//! async fn(&mut self) -> R // argument elided, implies `T=()` +//! async fn(&mut self, T) -> R +//! async fn(&mut self, T, &mut Context) -> R +//! where +//! Self: Model, +//! T: Clone + Send + 'static, +//! R: Send + 'static, +//! ``` +//! +//! Note that, due to type resolution ambiguities, non-async methods are not +//! allowed for replier ports. +//! +//! Input and replier ports will normally be exposed as public methods by a +//! [`Model`](crate::model::Model) so they can be connected to output and +//! requestor ports when assembling the simulation bench. However, input ports +//! may (and should) be defined as private methods if they are only used +//! internally by the model, for instance to schedule future actions on itself. +//! +//! Changing the signature of a public input or replier port is not considered +//! to alter the public interface of a model provided that the event, request +//! and reply types remain the same. In particular, adding a context argument or +//! changing a regular method to an `async` method will not cause idiomatic user +//! code to miscompile. +//! +//! #### Basic example +//! +//! ``` +//! use nexosim::model::{Context, Model}; +//! +//! pub struct MyModel { +//! // ... +//! } +//! impl MyModel { +//! pub fn my_input(&mut self, input: String, cx: &mut Context) { +//! // ... +//! } +//! pub async fn my_replier(&mut self, request: u32) -> bool { // context argument elided +//! // ... +//! # unimplemented!() +//! } +//! } +//! impl Model for MyModel {} +//! ``` +//! +//! ## Output and requestor ports +//! +//! Output and requestor ports can be added to a model using composition, adding +//! [`Output`], [`Requestor`] or [`UniRequestor`] objects as members. They are +//! parametrized by the event type, or by the request and reply types. +//! +//! Output ports broadcast events to all connected input ports, while requestor +//! ports broadcast queries to, and retrieve replies from, all connected replier +//! ports. //! //! On the surface, output and requestor ports only differ in that sending a //! query from a requestor port also returns an iterator over the replies from -//! all connected ports. Sending a query is more costly, however, because of the -//! need to wait until all connected models have processed the query. In -//! contrast, since events are buffered in the mailbox of the target model, -//! sending an event is a fire-and-forget operation. For this reason, output -//! ports should generally be preferred over requestor ports when possible. +//! all connected ports (or a single reply in the case of [`UniRequestor`]). +//! Sending a query is more costly, however, because of the need to wait until +//! the connected model(s) have processed the query. In contrast, since events +//! are buffered in the mailbox of the target model, sending an event is a +//! fire-and-forget operation. For this reason, output ports should generally be +//! preferred over requestor ports when possible. +//! +//! Models (or model prototypes, as appropriate) are expected to expose their +//! output and requestor ports as public members so they can be connected to +//! input and replier ports when assembling the simulation bench. Internal ports +//! used by hierarchical models to communicate with submodels are an exception +//! to this rule and are typically private. +//! +//! #### Basic example //! -//! `Output` and `Requestor` ports are clonable. Their clones are shallow -//! copies, meaning that any modification of the ports connected to one clone is -//! immediately reflected in other clones. +//! ``` +//! use nexosim::model::Model; +//! use nexosim::ports::{Output, Requestor}; +//! +//! pub struct MyModel { +//! pub my_output: Output, +//! pub my_requestor: Requestor, +//! } +//! impl MyModel { +//! // ... +//! } +//! impl Model for MyModel {} +//! ``` //! -//! #### Example +//! #### Example with cloned ports //! -//! This example demonstrates two submodels inside a parent model. The output of -//! the submodel and of the main model are clones and remain therefore always -//! connected to the same inputs. +//! [`Output`] and [`Requestor`] ports are clonable. The clones are shallow +//! copies, meaning that any modification of the ports connected to one instance +//! is immediately reflected by its clones. //! -//! For a more comprehensive example demonstrating hierarchical model -//! assemblies, see the [`assembly example`][assembly]. +//! Clones of output and requestor ports should be used with care: even though +//! they uphold the usual [ordering +//! guaranties](crate#message-ordering-guarantees), their use can lead to +//! somewhat surprising message orderings. //! -//! [assembly]: -//! https://github.com/asynchronics/nexosim/tree/main/nexosim/examples/assembly.rs +//! For instance, in the below [hierarchical +//! model](crate::model#hierarchical-models), while message `M0` is guaranteed +//! to reach the cloned output first, the relative arrival order of messages +//! `M1` and `M2` forwarded by the submodels to the cloned output is not +//! guaranteed. //! //! ``` //! use nexosim::model::{BuildContext, Model, ProtoModel}; //! use nexosim::ports::Output; //! use nexosim::simulation::Mailbox; //! -//! pub struct ChildModel { -//! pub output: Output, +//! pub struct ParentModel { +//! output: Output, +//! to_child1: Output, +//! to_child2: Output, //! } +//! impl ParentModel { +//! pub async fn trigger(&mut self) { +//! // M0 is guaranteed to reach the cloned output first. +//! self.output.send("M0".to_string()).await; //! -//! impl ChildModel { -//! pub fn new(output: Output) -> Self { -//! Self { -//! output, -//! } +//! // M1 and M2 are forwarded by `Child1` and `Child2` to the cloned output +//! // but may reach it in any relative order. +//! self.to_child1.send("M1".to_string()).await; +//! self.to_child2.send("M2".to_string()).await; //! } //! } -//! -//! impl Model for ChildModel {} -//! -//! pub struct ParentModel { -//! output: Output, -//! } -//! //! impl Model for ParentModel {} //! //! pub struct ProtoParentModel { -//! pub output: Output, +//! pub output: Output, //! } -//! //! impl ProtoParentModel { //! pub fn new() -> Self { //! Self { @@ -65,20 +184,86 @@ //! } //! } //! } -//! //! impl ProtoModel for ProtoParentModel { //! type Model = ParentModel; -//! //! fn build(self, cx: &mut BuildContext) -> ParentModel { -//! let mut child = ChildModel::new(self.output.clone()); +//! let child1 = ChildModel { output: self.output.clone() }; +//! let child2 = ChildModel { output: self.output.clone() }; +//! let mut parent = ParentModel { +//! output: self.output, +//! to_child1: Output::default(), +//! to_child2: Output::default(), +//! }; +//! +//! let child1_mailbox = Mailbox::new(); +//! let child2_mailbox = Mailbox::new(); +//! parent +//! .to_child1 +//! .connect(ChildModel::input, child1_mailbox.address()); +//! parent +//! .to_child2 +//! .connect(ChildModel::input, child2_mailbox.address()); //! -//! cx.add_submodel(child, Mailbox::new(), "child"); +//! cx.add_submodel(child1, child1_mailbox, "child1"); +//! cx.add_submodel(child2, child2_mailbox, "child2"); //! -//! ParentModel { output: self.output } +//! parent //! } //! } +//! +//! pub struct ChildModel { +//! pub output: Output, +//! } +//! impl ChildModel { +//! async fn input(&mut self, msg: String) { +//! self.output.send(msg).await; +//! } +//! } +//! impl Model for ChildModel {} //! ``` - +//! +//! # Simulation endpoints +//! +//! Simulation endpoints can be seen as entry and exit ports for a simulation +//! bench. +//! +//! [`EventSource`] and [`QuerySource`] objects are similar to [`Output`] and +//! [`Requestor`] ports, respectively. They can be connected to models and can +//! be used to send events or queries to such models via +//! [`Action`](crate::simulation::Action)s. +//! +//! Objects implementing the [`EventSink`] trait, such as [`EventSlot`] and +//! [`EventBuffer`], are in turn similar to input ports. They can be connected +//! to model outputs and collect events sent by such models. +//! +//! +//! # Connections +//! +//! Model ports can be connected to other model ports and to simulation +//! endpoints using the `*connect` family of methods exposed by [`Output`], +//! [`Requestor`], [`UniRequestor`], [`EventSource`] and [`QuerySource`]. +//! +//! Regular connections between two ports are made with the appropriate +//! `connect` method (for instance [`Output::connect`]). Such connections +//! broadcast events or requests from a sender port to one or several receiver +//! ports, cloning the event or request if necessary. +//! +//! Sometimes, however, it may be necessary to also map the event or request to +//! another type so it can be understood by the receiving port. While this +//! translation could be done by a model placed between the two ports, the +//! `map_connect` methods (for instance [`Output::map_connect`]) provide a +//! lightweight and computationally efficient alternative. These methods take a +//! mapping closure as argument which maps outgoing messages, and in the case of +//! requestors, a mapping closure which maps replies. +//! +//! Finally, it is sometime necessary to only forward messages or requests that +//! satisfy specific criteria. For instance, a model of a data bus may be +//! connected to many models (the "peripherals"), but its messages are usually +//! only addressed to selected models. The `filter_map_connect` methods (for +//! instance [`Output::filter_map_connect`]) enable this use-case by accepting a +//! closure that inspects the messages and determines whether they should be +//! forwarded, possibly after being mapped to another type. +//! mod input; mod output; mod sink; diff --git a/nexosim/src/ports/input/model_fn.rs b/nexosim/src/ports/input/model_fn.rs index 550bb85..771c10e 100644 --- a/nexosim/src/ports/input/model_fn.rs +++ b/nexosim/src/ports/input/model_fn.rs @@ -9,25 +9,20 @@ use super::markers; /// A function, method or closures that can be used as an *input port*. /// /// This trait is in particular implemented for any function or method with the -/// following signature, where it is implicitly assumed that the function -/// implements `Send + 'static`: +/// following signature, where the futures returned by the `async` variants must +/// implement `Send`: /// /// ```ignore -/// FnOnce(&mut M, T) -/// FnOnce(&mut M, T, &mut Context) +/// fn(&mut M) // argument elided, implies `T=()` +/// fn(&mut M, T) +/// fn(&mut M, T, &mut Context) +/// async fn(&mut M) // argument elided, implies `T=()` /// async fn(&mut M, T) /// async fn(&mut M, T, &mut Context) /// where -/// M: Model -/// ``` -/// -/// It is also implemented for the following signatures when `T=()`: -/// -/// ```ignore -/// FnOnce(&mut M) -/// async fn(&mut M) -/// where -/// M: Model +/// M: Model, +/// T: Clone + Send + 'static, +/// R: Send + 'static, /// ``` pub trait InputFn<'a, M: Model, T, S>: Send + 'static { /// The `Future` returned by the asynchronous method. @@ -121,22 +116,16 @@ where /// A function, method or closure that can be used as a *replier port*. /// /// This trait is in particular implemented for any function or method with the -/// following signature, where it is implicitly assumed that the function -/// implements `Send + 'static`: +/// following signature, where the returned futures must implement `Send`: /// /// ```ignore +/// async fn(&mut M) -> R // argument elided, implies `T=()` /// async fn(&mut M, T) -> R /// async fn(&mut M, T, &mut Context) -> R /// where -/// M: Model -/// ``` -/// -/// It is also implemented for the following signatures when `T=()`: -/// -/// ```ignore -/// async fn(&mut M) -> R -/// where -/// M: Model +/// M: Model, +/// T: Clone + Send + 'static, +/// R: Send + 'static, /// ``` pub trait ReplierFn<'a, M: Model, T, R, S>: Send + 'static { /// The `Future` returned by the asynchronous method. diff --git a/nexosim/src/ports/sink.rs b/nexosim/src/ports/sink.rs index e0fc0d1..b0b6743 100644 --- a/nexosim/src/ports/sink.rs +++ b/nexosim/src/ports/sink.rs @@ -22,7 +22,7 @@ pub trait EventSinkWriter: Clone + Send + Sync + 'static { /// An iterator over collected events with the ability to pause and resume event /// collection. /// -/// An `EventSinkStream` will typically be implemented on an `EventSink` for +/// An `EventSinkStream` will typically be implemented on an [`EventSink`] for /// which it will constitute a draining iterator. pub trait EventSinkStream: Iterator { /// Starts or resumes the collection of new events. diff --git a/nexosim/src/ports/sink/event_buffer.rs b/nexosim/src/ports/sink/event_buffer.rs index 1cc8718..e5fe2b0 100644 --- a/nexosim/src/ports/sink/event_buffer.rs +++ b/nexosim/src/ports/sink/event_buffer.rs @@ -12,7 +12,8 @@ struct Inner { buffer: Mutex>, } -/// An [`EventSink`] and [`EventSinkStream`] with a bounded size. +/// An iterator implementing [`EventSink`] and [`EventSinkStream`], backed by a +/// fixed-capacity buffer. /// /// If the maximum capacity is exceeded, older events are overwritten. Events /// are returned in first-in-first-out order. Note that even if the iterator diff --git a/nexosim/src/ports/sink/event_slot.rs b/nexosim/src/ports/sink/event_slot.rs index 135242f..a923f7c 100644 --- a/nexosim/src/ports/sink/event_slot.rs +++ b/nexosim/src/ports/sink/event_slot.rs @@ -10,7 +10,8 @@ struct Inner { slot: Mutex>, } -/// An [`EventSink`] and [`EventSinkStream`] that only keeps the last event. +/// An iterator implementing [`EventSink`] and [`EventSinkStream`] that only +/// keeps the last event. /// /// Once the value is read, the iterator will return `None` until a new value is /// received. If the slot contains a value when a new value is received, the diff --git a/nexosim/src/ports/source.rs b/nexosim/src/ports/source.rs index dff1d04..4d3f6b7 100644 --- a/nexosim/src/ports/source.rs +++ b/nexosim/src/ports/source.rs @@ -188,13 +188,13 @@ impl fmt::Debug for EventSource { } } -/// A request source port. +/// A query source port. /// /// The `QuerySource` port is similar to an -/// [`Requestor`](crate::ports::Requestor) port in that it can send events to -/// connected input ports. It is not meant, however, to be instantiated as a -/// member of a model, but rather as a simulation monitoring endpoint -/// instantiated during bench assembly. +/// [`Requestor`](crate::ports::Requestor) port in that it can send requests to +/// connected replier ports and receive replies. It is not meant, however, to be +/// instantiated as a member of a model, but rather as a simulation monitoring +/// endpoint instantiated during bench assembly. pub struct QuerySource { broadcaster: QueryBroadcaster, } diff --git a/nexosim/src/registry.rs b/nexosim/src/registry.rs index 8880c80..a8b6a01 100644 --- a/nexosim/src/registry.rs +++ b/nexosim/src/registry.rs @@ -1,7 +1,8 @@ //! Registry for sinks and sources. //! //! This module provides the `EndpointRegistry` object which associates each -//! event sink, event source and query source to a unique name. +//! event sink, event source and query source in a simulation bench to a unique +//! name. mod event_sink_registry; mod event_source_registry; @@ -15,8 +16,7 @@ pub(crate) use event_sink_registry::EventSinkRegistry; pub(crate) use event_source_registry::EventSourceRegistry; pub(crate) use query_source_registry::QuerySourceRegistry; -/// A registry that holds all sources and sinks meant to be accessed through -/// bindings or remote procedure calls. +/// A registry that holds the sources and sinks of a simulation bench. #[derive(Default, Debug)] pub struct EndpointRegistry { pub(crate) event_sink_registry: EventSinkRegistry, diff --git a/nexosim/src/registry/event_sink_registry.rs b/nexosim/src/registry/event_sink_registry.rs index 3722e6f..36da5f2 100644 --- a/nexosim/src/registry/event_sink_registry.rs +++ b/nexosim/src/registry/event_sink_registry.rs @@ -50,7 +50,7 @@ impl fmt::Debug for EventSinkRegistry { /// A type-erased `EventSinkStream`. pub(crate) trait EventSinkStreamAny: Send + 'static { /// Human-readable name of the event type, as returned by - /// `any::type_name()`. + /// `any::type_name`. fn event_type_name(&self) -> &'static str; /// Starts or resumes the collection of new events. diff --git a/nexosim/src/registry/event_source_registry.rs b/nexosim/src/registry/event_source_registry.rs index 848e361..5e72b93 100644 --- a/nexosim/src/registry/event_source_registry.rs +++ b/nexosim/src/registry/event_source_registry.rs @@ -92,7 +92,7 @@ pub(crate) trait EventSourceAny: Send + Sync + 'static { ) -> Result<(Action, ActionKey), DeserializationError>; /// Human-readable name of the event type, as returned by - /// `any::type_name()`. + /// `any::type_name`. fn event_type_name(&self) -> &'static str; } diff --git a/nexosim/src/registry/query_source_registry.rs b/nexosim/src/registry/query_source_registry.rs index eeed7c5..0f45843 100644 --- a/nexosim/src/registry/query_source_registry.rs +++ b/nexosim/src/registry/query_source_registry.rs @@ -68,11 +68,11 @@ pub(crate) trait QuerySourceAny: Send + Sync + 'static { ) -> Result<(Action, Box), DeserializationError>; /// Human-readable name of the request type, as returned by - /// `any::type_name()`. + /// `any::type_name`. fn request_type_name(&self) -> &'static str; /// Human-readable name of the reply type, as returned by - /// `any::type_name()`. + /// `any::type_name`. fn reply_type_name(&self) -> &'static str; } diff --git a/nexosim/src/server/services/controller_service.rs b/nexosim/src/server/services/controller_service.rs index ae93e0e..ccf8f09 100644 --- a/nexosim/src/server/services/controller_service.rs +++ b/nexosim/src/server/services/controller_service.rs @@ -31,7 +31,7 @@ impl ControllerService { /// that event as well as all other events scheduled for the same time. /// /// Processing is gated by a (possibly blocking) call to - /// [`Clock::synchronize()`](crate::time::Clock::synchronize) on the + /// [`Clock::synchronize`](crate::time::Clock::synchronize) on the /// configured simulation clock. This method blocks until all newly /// processed events have completed. pub(crate) fn step(&mut self, _request: StepRequest) -> StepReply { @@ -59,7 +59,7 @@ impl ControllerService { /// Iteratively advances the simulation time until the specified deadline, /// as if by calling - /// [`Simulation::step()`](crate::simulation::Simulation::step) repeatedly. + /// [`Simulation::step`](crate::simulation::Simulation::step) repeatedly. /// /// This method blocks until all events scheduled up to the specified target /// time have completed. The simulation time upon completion is equal to the diff --git a/nexosim/src/simulation.rs b/nexosim/src/simulation.rs index 4bc52d2..c4e9193 100644 --- a/nexosim/src/simulation.rs +++ b/nexosim/src/simulation.rs @@ -13,10 +13,10 @@ //! 2. connection of the models' output/requestor ports to input/replier ports //! using the [`Address`]es of the target models, //! 3. instantiation of a [`SimInit`] simulation builder and migration of all -//! models and mailboxes to the builder with [`SimInit::add_model()`], -//! 4. initialization of a [`Simulation`] instance with [`SimInit::init()`], +//! models and mailboxes to the builder with [`SimInit::add_model`], +//! 4. initialization of a [`Simulation`] instance with [`SimInit::init`], //! possibly preceded by the setup of a custom clock with -//! [`SimInit::set_clock()`], +//! [`SimInit::set_clock`], //! 5. discrete-time simulation, which typically involves scheduling events and //! incrementing simulation time while observing the models outputs. //! @@ -42,8 +42,8 @@ //! //! The default capacity should prove a reasonable trade-off in most cases, but //! for situations where it is not appropriate, it is possible to instantiate -//! mailboxes with a custom capacity by using [`Mailbox::with_capacity()`] -//! instead of [`Mailbox::new()`]. +//! mailboxes with a custom capacity by using [`Mailbox::with_capacity`] instead +//! of [`Mailbox::new`]. //! //! ## Avoiding deadlocks //! @@ -68,14 +68,13 @@ //! //! The second scenario is rare in well-behaving models and if it occurs, it is //! most typically at the very beginning of a simulation when models -//! simultaneously and mutually send events during the call to -//! [`Model::init()`](crate::model::Model::init). If such a large amount of -//! events is deemed normal behavior, the issue can be remedied by increasing -//! the capacity of the saturated mailboxes. +//! simultaneously and mutually send events during the call to [`Model::init`]. +//! If such a large amount of events is deemed normal behavior, the issue can be +//! remedied by increasing the capacity of the saturated mailboxes. //! -//! Any deadlocks will be reported as an [`ExecutionError::Deadlock`] error, -//! which identifies all involved models and the amount of unprocessed messages -//! (events or requests) in their mailboxes. +//! Deadlocks are reported as [`ExecutionError::Deadlock`] errors, which +//! identify all involved models and the count of unprocessed messages (events +//! or requests) in their mailboxes. mod mailbox; mod scheduler; mod sim_init; @@ -118,7 +117,7 @@ thread_local! { pub(crate) static CURRENT_MODEL_ID: Cell = const { Cell /// Simulation environment. /// /// A `Simulation` is created by calling -/// [`SimInit::init()`](crate::simulation::SimInit::init) on a simulation +/// [`SimInit::init`](crate::simulation::SimInit::init) on a simulation /// initializer. It contains an asynchronous executor that runs all simulation /// models added beforehand to [`SimInit`]. /// @@ -126,31 +125,31 @@ thread_local! { pub(crate) static CURRENT_MODEL_ID: Cell = const { Cell /// simulation time. The scheduling queue can be accessed from the simulation /// itself, but also from models via the optional [`&mut /// Context`](crate::model::Context) argument of input and replier port methods. -/// Likewise, simulation time can be accessed with the [`Simulation::time()`] +/// Likewise, simulation time can be accessed with the [`Simulation::time`] /// method, or from models with the -/// [`Context::time()`](crate::simulation::Context::time) method. +/// [`Context::time`](crate::simulation::Context::time) method. /// /// Events and queries can be scheduled immediately, *i.e.* for the current -/// simulation time, using [`process_event()`](Simulation::process_event) and -/// [`send_query()`](Simulation::process_query). Calling these methods will -/// block until all computations triggered by such event or query have -/// completed. In the case of queries, the response is returned. +/// simulation time, using [`process_event`](Simulation::process_event) and +/// [`send_query`](Simulation::process_query). Calling these methods will block +/// until all computations triggered by such event or query have completed. In +/// the case of queries, the response is returned. /// /// Events can also be scheduled at a future simulation time using one of the -/// [`schedule_*()`](Scheduler::schedule_event) method. These methods queue an +/// [`schedule_*`](Scheduler::schedule_event) method. These methods queue an /// event without blocking. /// /// Finally, the [`Simulation`] instance manages simulation time. A call to -/// [`step()`](Simulation::step) will: +/// [`step`](Simulation::step) will: /// /// 1. increment simulation time until that of the next scheduled event in /// chronological order, then -/// 2. call [`Clock::synchronize()`](crate::time::Clock::synchronize) which, unless the -/// simulation is configured to run as fast as possible, blocks until the -/// desired wall clock time, and finally +/// 2. call [`Clock::synchronize`] which, unless the simulation is configured to +/// run as fast as possible, blocks until the desired wall clock time, and +/// finally /// 3. run all computations scheduled for the new simulation time. /// -/// The [`step_until()`](Simulation::step_until) method operates similarly but +/// The [`step_until`](Simulation::step_until) method operates similarly but /// iterates until the target simulation time has been reached. pub struct Simulation { executor: Executor, @@ -216,15 +215,14 @@ impl Simulation { /// that event as well as all other events scheduled for the same time. /// /// Processing is gated by a (possibly blocking) call to - /// [`Clock::synchronize()`](crate::time::Clock::synchronize) on the configured - /// simulation clock. This method blocks until all newly processed events - /// have completed. + /// [`Clock::synchronize`] on the configured simulation clock. This method + /// blocks until all newly processed events have completed. pub fn step(&mut self) -> Result<(), ExecutionError> { self.step_to_next(None).map(|_| ()) } /// Iteratively advances the simulation time until the specified deadline, - /// as if by calling [`Simulation::step()`] repeatedly. + /// as if by calling [`Simulation::step`] repeatedly. /// /// This method blocks until all events scheduled up to the specified target /// time have completed. The simulation time upon completion is equal to the @@ -240,7 +238,7 @@ impl Simulation { } /// Iteratively advances the simulation time, as if by calling - /// [`Simulation::step()`] repeatedly. + /// [`Simulation::step`] repeatedly. /// /// This method blocks until all events scheduled have completed. pub fn step_unbounded(&mut self) -> Result<(), ExecutionError> { diff --git a/nexosim/src/simulation/mailbox.rs b/nexosim/src/simulation/mailbox.rs index 76eb84d..37c2eba 100644 --- a/nexosim/src/simulation/mailbox.rs +++ b/nexosim/src/simulation/mailbox.rs @@ -8,7 +8,7 @@ use crate::model::Model; /// A mailbox is an entity associated to a model instance that collects all /// messages sent to that model. The size of its internal buffer can be /// optionally specified at construction time using -/// [`with_capacity()`](Mailbox::with_capacity). +/// [`with_capacity`](Mailbox::with_capacity). pub struct Mailbox(pub(crate) Receiver); impl Mailbox { @@ -58,7 +58,7 @@ impl fmt::Debug for Mailbox { /// For the sake of convenience, methods that require an address by value will /// typically also accept an `&Address` or an `&Mailbox` since these references /// implement the `Into
` trait, automatically invoking -/// `Address::clone()` or `Mailbox::address()` as appropriate. +/// `Address::clone` or `Mailbox::address` as appropriate. pub struct Address(pub(crate) Sender); impl Clone for Address { @@ -80,8 +80,7 @@ impl From<&Address> for Address { impl From<&Mailbox> for Address { /// Converts a [Mailbox] reference into an [`Address`]. /// - /// This calls [`Mailbox::address()`] on the mailbox and returns the - /// address. + /// This calls [`Mailbox::address`] on the mailbox and returns the address. #[inline] fn from(s: &Mailbox) -> Address { s.address() diff --git a/nexosim/src/simulation/scheduler.rs b/nexosim/src/simulation/scheduler.rs index e24ffca..97be2dc 100644 --- a/nexosim/src/simulation/scheduler.rs +++ b/nexosim/src/simulation/scheduler.rs @@ -25,7 +25,9 @@ use crate::{time::TearableAtomicTime, util::sync_cell::SyncCell}; const GLOBAL_SCHEDULER_ORIGIN_ID: usize = 0; -/// A global scheduler. +/// A global simulation scheduler. +/// +/// A `Scheduler` can be `Clone`d and sent to other threads. #[derive(Clone)] pub struct Scheduler(GlobalScheduler); @@ -40,9 +42,6 @@ impl Scheduler { /// Returns the current simulation time. /// - /// Beware that, if the scheduler runs in a separate thread as the - /// simulation, the time may change concurrently. - /// /// # Examples /// /// ``` @@ -200,9 +199,9 @@ impl fmt::Debug for Scheduler { /// Managed handle to a scheduled action. /// /// An `AutoActionKey` is a managed handle to a scheduled action that cancels -/// action on drop. +/// its associated action on drop. #[derive(Debug)] -#[must_use = "managed action key shall be used"] +#[must_use = "dropping this key immediately cancels the associated action"] pub struct AutoActionKey { is_cancelled: Arc, } @@ -295,6 +294,12 @@ impl Error for SchedulingError {} /// A possibly periodic, possibly cancellable action that can be scheduled or /// processed immediately. +/// +/// `Actions` can be created from an [`EventSource`](crate::ports::EventSource) +/// or [`QuerySource`](crate::ports::QuerySource). They can be used to schedule +/// events and requests with [`Scheduler::schedule`], or to process events and +/// requests immediately with +/// [`Simulation::process`](crate::simulation::Simulation::process). pub struct Action { inner: Box, } diff --git a/nexosim/src/simulation/sim_init.rs b/nexosim/src/simulation/sim_init.rs index 7200320..09a75cd 100644 --- a/nexosim/src/simulation/sim_init.rs +++ b/nexosim/src/simulation/sim_init.rs @@ -151,7 +151,7 @@ impl SimInit { } /// Builds a simulation initialized at the specified simulation time, - /// executing the [`Model::init()`](crate::model::Model::init) method on all + /// executing the [`Model::init`](crate::model::Model::init) method on all /// model initializers. /// /// The simulation object and its associated scheduler are returned upon diff --git a/nexosim/src/time.rs b/nexosim/src/time.rs index b135119..d0a14cc 100644 --- a/nexosim/src/time.rs +++ b/nexosim/src/time.rs @@ -1,4 +1,4 @@ -//! Simulation time and scheduling. +//! Simulation time and clocks. //! //! This module provides most notably: //! diff --git a/nexosim/src/time/clock.rs b/nexosim/src/time/clock.rs index a93ac93..7a86b07 100644 --- a/nexosim/src/time/clock.rs +++ b/nexosim/src/time/clock.rs @@ -10,7 +10,7 @@ use crate::time::MonotonicTime; /// as-fast-as-possible and real-time clocks. /// /// A clock can be associated to a simulation prior to initialization by calling -/// [`SimInit::set_clock()`](crate::simulation::SimInit::set_clock). +/// [`SimInit::set_clock`](crate::simulation::SimInit::set_clock). pub trait Clock: Send { /// Blocks until the deadline. fn synchronize(&mut self, deadline: MonotonicTime) -> SyncStatus; @@ -107,7 +107,7 @@ impl SystemClock { /// The provided reference time may lie in the past or in the future. /// /// Note that, even though the wall clock reference is specified with the - /// (non-monotonic) system clock, the [`synchronize()`](Clock::synchronize) + /// (non-monotonic) system clock, the [`synchronize`](Clock::synchronize) /// method will still use the system's _monotonic_ clock. This constructor /// makes a best-effort attempt at synchronizing the monotonic clock with /// the non-monotonic system clock _at construction time_, but this @@ -164,8 +164,8 @@ impl Clock for SystemClock { /// monotonic clock. /// /// This clock is similar to [`SystemClock`] except that the first call to -/// [`synchronize()`](Clock::synchronize) never blocks and implicitly defines -/// the reference time. In other words, the clock starts running on its first +/// [`synchronize`](Clock::synchronize) never blocks and implicitly defines the +/// reference time. In other words, the clock starts running on its first /// invocation. #[derive(Copy, Clone, Debug, Default)] pub struct AutoSystemClock { diff --git a/nexosim/src/tracing.rs b/nexosim/src/tracing.rs index ef590b9..e6817fd 100644 --- a/nexosim/src/tracing.rs +++ b/nexosim/src/tracing.rs @@ -69,7 +69,7 @@ //! 2024-09-10T14:39:24.670921Z INFO my_simulation: something happened outside the simulation //! ``` //! -//! Alternatively, `SimulationTime::with_system_timer_always()` can be used to +//! Alternatively, `SimulationTime::with_system_timer_always` can be used to //! always prepend the system time even for simulation events: //! //! ```text