pub struct Runner<'a, M, E = Infallible>where
M: Machine<E>,{ /* private fields */ }runtime only.Expand description
Owns the current mode and feeds events into a Machine.
Runner is intentionally small. It is a convenience layer around the core
contracts, not a framework.
Implementations§
Source§impl<'a, M, E> Runner<'a, M, E>where
M: Machine<E>,
impl<'a, M, E> Runner<'a, M, E>where
M: Machine<E>,
Sourcepub fn new(machine: &'a M) -> Self
pub fn new(machine: &'a M) -> Self
Creates a new runner starting at machine.initial_mode().
Examples found in repository?
135fn main() {
136 let system = System;
137 let runner = Runner::new(&system);
138 let mut metrics = MetricsRunner::new(runner);
139
140 let events = [
141 Event::Start,
142 Event::Fault,
143 Event::Recover,
144 Event::Start,
145 Event::Fault,
146 Event::Recover,
147 ];
148
149 for event in &events {
150 println!("Event: {event:?}");
151 metrics.feed(event);
152 }
153
154 metrics.print_summary();
155
156 assert_eq!(metrics.transition_count, 6);
157 assert_eq!(metrics.commands_dispatched, 6);
158}More examples
73fn main() {
74 let system = System;
75 let policy = ForwardOnly;
76 let mut runner = Runner::new(&system);
77
78 println!("Starting in {:?}", runner.mode());
79
80 // Allowed: Ready → Active
81 let commands = runner.feed_checked(&Event::Start, &policy);
82 println!("\nEvent: Start");
83 println!(" mode: {:?}, commands: {:?}", runner.mode(), commands);
84
85 // Denied: Active → Ready (policy blocks backward transitions)
86 let result = runner.feed_checked(&Event::Reset, &policy);
87 println!("\nEvent: Reset");
88 match &result {
89 Ok(cmds) => println!(" mode: {:?}, commands: {cmds:?}", runner.mode()),
90 Err(err) => println!(" denied: {err}"),
91 }
92 println!(" mode unchanged: {:?}", runner.mode());
93
94 // Allowed: Active → Safe
95 let commands = runner.feed_checked(&Event::Fault, &policy);
96 println!("\nEvent: Fault");
97 println!(" mode: {:?}, commands: {:?}", runner.mode(), commands);
98}98fn main() {
99 let system = SensorSystem;
100 let mut runner = Runner::new(&system);
101
102 let (tx, rx) = mpsc::channel();
103
104 spawn_timer(tx.clone());
105 spawn_sensor(tx.clone());
106 spawn_fault_injector(tx);
107
108 println!("Starting in {:?}", runner.mode());
109
110 loop {
111 match rx.recv_timeout(Duration::from_millis(500)) {
112 Ok(event) => {
113 let before = runner.mode().clone();
114 runner.feed_and_dispatch(&event, |cmd| {
115 println!(" dispatch: {cmd:?}");
116 });
117 let after = runner.mode();
118 println!("{before:?} + {event:?} => {after:?}");
119
120 if *after == Mode::Safe {
121 println!("Reached Safe mode, shutting down.");
122 break;
123 }
124 }
125 Err(mpsc::RecvTimeoutError::Timeout) => {
126 println!("No events for 500ms, shutting down.");
127 break;
128 }
129 Err(mpsc::RecvTimeoutError::Disconnected) => {
130 println!("All senders dropped, shutting down.");
131 break;
132 }
133 }
134 }
135
136 println!("Final mode: {:?}", runner.mode());
137}63fn main() {
64 let system = RecoverableSystem;
65 let mut runner = Runner::new(&system);
66 let max_recoveries: u32 = 3;
67 let mut recovery_count: u32 = 0;
68
69 println!("Starting in {:?}", runner.mode());
70
71 // Drive: Ready -> Active -> Safe -> Ready -> Active -> ...
72 loop {
73 // Ready -> Active
74 runner.feed_and_dispatch(&Event::Activate, |cmd| {
75 println!(" dispatch: {cmd:?}");
76 });
77 println!("Mode: {:?}", runner.mode());
78
79 // Active -> Safe (fault)
80 runner.feed_and_dispatch(&Event::Fault, |cmd| {
81 println!(" dispatch: {cmd:?}");
82 });
83 println!("Mode: {:?} (fault!)", runner.mode());
84
85 recovery_count = recovery_count.saturating_add(1);
86 println!("Recovery #{recovery_count}");
87
88 if recovery_count >= max_recoveries {
89 println!("Max recoveries ({max_recoveries}) reached, staying in Safe.");
90 break;
91 }
92
93 // Safe -> Ready (recover)
94 runner.feed_and_dispatch(&Event::RecoveryComplete, |cmd| {
95 println!(" dispatch: {cmd:?}");
96 });
97 println!("Mode: {:?} (recovered)", runner.mode());
98 }
99
100 println!(
101 "Final mode: {:?}, total recoveries: {recovery_count}",
102 runner.mode()
103 );
104 assert_eq!(runner.mode(), &Mode::Safe);
105 assert_eq!(recovery_count, max_recoveries);
106}75fn main() {
76 let instrument = LabInstrument;
77 let mut runner = Runner::new(&instrument);
78
79 let events = [
80 Event::PowerOn,
81 Event::SensorReady,
82 Event::BeginMeasurement,
83 Event::DataCollected,
84 Event::DataCollected,
85 Event::AnomalyDetected,
86 Event::OperatorReset,
87 Event::BeginMeasurement,
88 ];
89
90 // Record every transition in the journal
91 let mut journal: InMemoryJournal<Mode, Event, Command> = InMemoryJournal::new();
92
93 println!("--- Recording transitions ---\n");
94 for event in events {
95 let from = runner.mode().clone();
96 let commands = runner.feed(&event);
97 let to = runner.mode().clone();
98
99 println!(" {from:?} + {event:?} => {to:?}");
100 if !commands.is_empty() {
101 println!(" commands: {commands:?}");
102 }
103
104 journal.record_step(&from, &to, &event, &commands);
105 }
106
107 println!("\n--- Journal: {} records ---\n", journal.len());
108 for record in journal.records() {
109 println!(
110 " step {}: {:?} => {:?} (via {:?})",
111 record.step, record.from, record.to, record.event,
112 );
113 }
114
115 // Replay: feed the same events through a fresh machine and verify
116 println!("\n--- Replaying ---\n");
117 match journal.replay(&instrument, instrument.initial_mode()) {
118 Ok(final_mode) => {
119 println!(" replay verified: final mode = {final_mode:?}");
120 assert_eq!(final_mode, runner.mode().clone());
121 }
122 Err(err) => {
123 println!(" {err}");
124 }
125 }
126}Sourcepub const fn with_mode(machine: &'a M, mode: M::Mode) -> Self
pub const fn with_mode(machine: &'a M, mode: M::Mode) -> Self
Creates a runner with an explicit initial mode.
Sourcepub const fn mode(&self) -> &M::Mode
pub const fn mode(&self) -> &M::Mode
Returns the current mode.
Examples found in repository?
77 fn new(runner: Runner<'a, System>) -> Self {
78 let initial = runner.mode().clone();
79 let mut mode_entries = HashMap::new();
80 *mode_entries.entry(initial).or_insert(0) += 1;
81
82 Self {
83 runner,
84 transition_count: 0,
85 commands_dispatched: 0,
86 mode_entries,
87 mode_entered_at: Instant::now(),
88 mode_durations: HashMap::new(),
89 }
90 }
91
92 fn feed(&mut self, event: &Event) {
93 let before = self.runner.mode().clone();
94 let commands = self.runner.feed(event);
95
96 let after = self.runner.mode().clone();
97
98 self.commands_dispatched += commands.len() as u64;
99
100 if before != after {
101 self.transition_count += 1;
102
103 let elapsed = self.mode_entered_at.elapsed();
104 *self.mode_durations.entry(before).or_default() += elapsed;
105 self.mode_entered_at = Instant::now();
106
107 *self.mode_entries.entry(after.clone()).or_insert(0) += 1;
108 println!(" [metrics] transition to {after:?}");
109 }
110
111 for cmd in &commands {
112 println!(" [metrics] dispatch: {cmd:?}");
113 }
114 }
115
116 fn print_summary(&self) {
117 println!("\n=== Metrics Summary ===");
118 println!("Transitions: {}", self.transition_count);
119 println!("Commands dispatched: {}", self.commands_dispatched);
120
121 println!("Mode entries:");
122 for (mode, count) in &self.mode_entries {
123 println!(" {mode:?}: {count}");
124 }
125
126 println!("Time in mode:");
127 for (mode, dur) in &self.mode_durations {
128 println!(" {mode:?}: {dur:?}");
129 }
130
131 println!("Current mode: {:?}", self.runner.mode());
132 }More examples
73fn main() {
74 let system = System;
75 let policy = ForwardOnly;
76 let mut runner = Runner::new(&system);
77
78 println!("Starting in {:?}", runner.mode());
79
80 // Allowed: Ready → Active
81 let commands = runner.feed_checked(&Event::Start, &policy);
82 println!("\nEvent: Start");
83 println!(" mode: {:?}, commands: {:?}", runner.mode(), commands);
84
85 // Denied: Active → Ready (policy blocks backward transitions)
86 let result = runner.feed_checked(&Event::Reset, &policy);
87 println!("\nEvent: Reset");
88 match &result {
89 Ok(cmds) => println!(" mode: {:?}, commands: {cmds:?}", runner.mode()),
90 Err(err) => println!(" denied: {err}"),
91 }
92 println!(" mode unchanged: {:?}", runner.mode());
93
94 // Allowed: Active → Safe
95 let commands = runner.feed_checked(&Event::Fault, &policy);
96 println!("\nEvent: Fault");
97 println!(" mode: {:?}, commands: {:?}", runner.mode(), commands);
98}98fn main() {
99 let system = SensorSystem;
100 let mut runner = Runner::new(&system);
101
102 let (tx, rx) = mpsc::channel();
103
104 spawn_timer(tx.clone());
105 spawn_sensor(tx.clone());
106 spawn_fault_injector(tx);
107
108 println!("Starting in {:?}", runner.mode());
109
110 loop {
111 match rx.recv_timeout(Duration::from_millis(500)) {
112 Ok(event) => {
113 let before = runner.mode().clone();
114 runner.feed_and_dispatch(&event, |cmd| {
115 println!(" dispatch: {cmd:?}");
116 });
117 let after = runner.mode();
118 println!("{before:?} + {event:?} => {after:?}");
119
120 if *after == Mode::Safe {
121 println!("Reached Safe mode, shutting down.");
122 break;
123 }
124 }
125 Err(mpsc::RecvTimeoutError::Timeout) => {
126 println!("No events for 500ms, shutting down.");
127 break;
128 }
129 Err(mpsc::RecvTimeoutError::Disconnected) => {
130 println!("All senders dropped, shutting down.");
131 break;
132 }
133 }
134 }
135
136 println!("Final mode: {:?}", runner.mode());
137}63fn main() {
64 let system = RecoverableSystem;
65 let mut runner = Runner::new(&system);
66 let max_recoveries: u32 = 3;
67 let mut recovery_count: u32 = 0;
68
69 println!("Starting in {:?}", runner.mode());
70
71 // Drive: Ready -> Active -> Safe -> Ready -> Active -> ...
72 loop {
73 // Ready -> Active
74 runner.feed_and_dispatch(&Event::Activate, |cmd| {
75 println!(" dispatch: {cmd:?}");
76 });
77 println!("Mode: {:?}", runner.mode());
78
79 // Active -> Safe (fault)
80 runner.feed_and_dispatch(&Event::Fault, |cmd| {
81 println!(" dispatch: {cmd:?}");
82 });
83 println!("Mode: {:?} (fault!)", runner.mode());
84
85 recovery_count = recovery_count.saturating_add(1);
86 println!("Recovery #{recovery_count}");
87
88 if recovery_count >= max_recoveries {
89 println!("Max recoveries ({max_recoveries}) reached, staying in Safe.");
90 break;
91 }
92
93 // Safe -> Ready (recover)
94 runner.feed_and_dispatch(&Event::RecoveryComplete, |cmd| {
95 println!(" dispatch: {cmd:?}");
96 });
97 println!("Mode: {:?} (recovered)", runner.mode());
98 }
99
100 println!(
101 "Final mode: {:?}, total recoveries: {recovery_count}",
102 runner.mode()
103 );
104 assert_eq!(runner.mode(), &Mode::Safe);
105 assert_eq!(recovery_count, max_recoveries);
106}75fn main() {
76 let instrument = LabInstrument;
77 let mut runner = Runner::new(&instrument);
78
79 let events = [
80 Event::PowerOn,
81 Event::SensorReady,
82 Event::BeginMeasurement,
83 Event::DataCollected,
84 Event::DataCollected,
85 Event::AnomalyDetected,
86 Event::OperatorReset,
87 Event::BeginMeasurement,
88 ];
89
90 // Record every transition in the journal
91 let mut journal: InMemoryJournal<Mode, Event, Command> = InMemoryJournal::new();
92
93 println!("--- Recording transitions ---\n");
94 for event in events {
95 let from = runner.mode().clone();
96 let commands = runner.feed(&event);
97 let to = runner.mode().clone();
98
99 println!(" {from:?} + {event:?} => {to:?}");
100 if !commands.is_empty() {
101 println!(" commands: {commands:?}");
102 }
103
104 journal.record_step(&from, &to, &event, &commands);
105 }
106
107 println!("\n--- Journal: {} records ---\n", journal.len());
108 for record in journal.records() {
109 println!(
110 " step {}: {:?} => {:?} (via {:?})",
111 record.step, record.from, record.to, record.event,
112 );
113 }
114
115 // Replay: feed the same events through a fresh machine and verify
116 println!("\n--- Replaying ---\n");
117 match journal.replay(&instrument, instrument.initial_mode()) {
118 Ok(final_mode) => {
119 println!(" replay verified: final mode = {final_mode:?}");
120 assert_eq!(final_mode, runner.mode().clone());
121 }
122 Err(err) => {
123 println!(" {err}");
124 }
125 }
126}Sourcepub fn feed(&mut self, event: &M::Event) -> Vec<M::Command>
pub fn feed(&mut self, event: &M::Event) -> Vec<M::Command>
Feeds an event into the machine, applies any mode change, and returns emitted commands.
This does not enforce a Policy. Use Runner::feed_checked
when you need explicit transition approval.
Examples found in repository?
92 fn feed(&mut self, event: &Event) {
93 let before = self.runner.mode().clone();
94 let commands = self.runner.feed(event);
95
96 let after = self.runner.mode().clone();
97
98 self.commands_dispatched += commands.len() as u64;
99
100 if before != after {
101 self.transition_count += 1;
102
103 let elapsed = self.mode_entered_at.elapsed();
104 *self.mode_durations.entry(before).or_default() += elapsed;
105 self.mode_entered_at = Instant::now();
106
107 *self.mode_entries.entry(after.clone()).or_insert(0) += 1;
108 println!(" [metrics] transition to {after:?}");
109 }
110
111 for cmd in &commands {
112 println!(" [metrics] dispatch: {cmd:?}");
113 }
114 }More examples
75fn main() {
76 let instrument = LabInstrument;
77 let mut runner = Runner::new(&instrument);
78
79 let events = [
80 Event::PowerOn,
81 Event::SensorReady,
82 Event::BeginMeasurement,
83 Event::DataCollected,
84 Event::DataCollected,
85 Event::AnomalyDetected,
86 Event::OperatorReset,
87 Event::BeginMeasurement,
88 ];
89
90 // Record every transition in the journal
91 let mut journal: InMemoryJournal<Mode, Event, Command> = InMemoryJournal::new();
92
93 println!("--- Recording transitions ---\n");
94 for event in events {
95 let from = runner.mode().clone();
96 let commands = runner.feed(&event);
97 let to = runner.mode().clone();
98
99 println!(" {from:?} + {event:?} => {to:?}");
100 if !commands.is_empty() {
101 println!(" commands: {commands:?}");
102 }
103
104 journal.record_step(&from, &to, &event, &commands);
105 }
106
107 println!("\n--- Journal: {} records ---\n", journal.len());
108 for record in journal.records() {
109 println!(
110 " step {}: {:?} => {:?} (via {:?})",
111 record.step, record.from, record.to, record.event,
112 );
113 }
114
115 // Replay: feed the same events through a fresh machine and verify
116 println!("\n--- Replaying ---\n");
117 match journal.replay(&instrument, instrument.initial_mode()) {
118 Ok(final_mode) => {
119 println!(" replay verified: final mode = {final_mode:?}");
120 assert_eq!(final_mode, runner.mode().clone());
121 }
122 Err(err) => {
123 println!(" {err}");
124 }
125 }
126}Sourcepub fn feed_checked<P>(
&mut self,
event: &M::Event,
policy: &P,
) -> Result<Vec<M::Command>, LifecycleError<M::Mode>>
pub fn feed_checked<P>( &mut self, event: &M::Event, policy: &P, ) -> Result<Vec<M::Command>, LifecycleError<M::Mode>>
Like Runner::feed, but checks policy before applying a mode change.
§Errors
Returns LifecycleError::TransitionDenied if the policy rejects the transition.
Examples found in repository?
73fn main() {
74 let system = System;
75 let policy = ForwardOnly;
76 let mut runner = Runner::new(&system);
77
78 println!("Starting in {:?}", runner.mode());
79
80 // Allowed: Ready → Active
81 let commands = runner.feed_checked(&Event::Start, &policy);
82 println!("\nEvent: Start");
83 println!(" mode: {:?}, commands: {:?}", runner.mode(), commands);
84
85 // Denied: Active → Ready (policy blocks backward transitions)
86 let result = runner.feed_checked(&Event::Reset, &policy);
87 println!("\nEvent: Reset");
88 match &result {
89 Ok(cmds) => println!(" mode: {:?}, commands: {cmds:?}", runner.mode()),
90 Err(err) => println!(" denied: {err}"),
91 }
92 println!(" mode unchanged: {:?}", runner.mode());
93
94 // Allowed: Active → Safe
95 let commands = runner.feed_checked(&Event::Fault, &policy);
96 println!("\nEvent: Fault");
97 println!(" mode: {:?}, commands: {:?}", runner.mode(), commands);
98}Sourcepub fn feed_and_dispatch<F>(&mut self, event: &M::Event, dispatch: F)
pub fn feed_and_dispatch<F>(&mut self, event: &M::Event, dispatch: F)
Feeds an event and dispatches each command to dispatch.
This is a convenience helper for the common “execute commands immediately” pattern. Commands are dispatched in the order they were emitted.
Examples found in repository?
98fn main() {
99 let system = SensorSystem;
100 let mut runner = Runner::new(&system);
101
102 let (tx, rx) = mpsc::channel();
103
104 spawn_timer(tx.clone());
105 spawn_sensor(tx.clone());
106 spawn_fault_injector(tx);
107
108 println!("Starting in {:?}", runner.mode());
109
110 loop {
111 match rx.recv_timeout(Duration::from_millis(500)) {
112 Ok(event) => {
113 let before = runner.mode().clone();
114 runner.feed_and_dispatch(&event, |cmd| {
115 println!(" dispatch: {cmd:?}");
116 });
117 let after = runner.mode();
118 println!("{before:?} + {event:?} => {after:?}");
119
120 if *after == Mode::Safe {
121 println!("Reached Safe mode, shutting down.");
122 break;
123 }
124 }
125 Err(mpsc::RecvTimeoutError::Timeout) => {
126 println!("No events for 500ms, shutting down.");
127 break;
128 }
129 Err(mpsc::RecvTimeoutError::Disconnected) => {
130 println!("All senders dropped, shutting down.");
131 break;
132 }
133 }
134 }
135
136 println!("Final mode: {:?}", runner.mode());
137}More examples
63fn main() {
64 let system = RecoverableSystem;
65 let mut runner = Runner::new(&system);
66 let max_recoveries: u32 = 3;
67 let mut recovery_count: u32 = 0;
68
69 println!("Starting in {:?}", runner.mode());
70
71 // Drive: Ready -> Active -> Safe -> Ready -> Active -> ...
72 loop {
73 // Ready -> Active
74 runner.feed_and_dispatch(&Event::Activate, |cmd| {
75 println!(" dispatch: {cmd:?}");
76 });
77 println!("Mode: {:?}", runner.mode());
78
79 // Active -> Safe (fault)
80 runner.feed_and_dispatch(&Event::Fault, |cmd| {
81 println!(" dispatch: {cmd:?}");
82 });
83 println!("Mode: {:?} (fault!)", runner.mode());
84
85 recovery_count = recovery_count.saturating_add(1);
86 println!("Recovery #{recovery_count}");
87
88 if recovery_count >= max_recoveries {
89 println!("Max recoveries ({max_recoveries}) reached, staying in Safe.");
90 break;
91 }
92
93 // Safe -> Ready (recover)
94 runner.feed_and_dispatch(&Event::RecoveryComplete, |cmd| {
95 println!(" dispatch: {cmd:?}");
96 });
97 println!("Mode: {:?} (recovered)", runner.mode());
98 }
99
100 println!(
101 "Final mode: {:?}, total recoveries: {recovery_count}",
102 runner.mode()
103 );
104 assert_eq!(runner.mode(), &Mode::Safe);
105 assert_eq!(recovery_count, max_recoveries);
106}Sourcepub fn feed_checked_and_dispatch<P, F>(
&mut self,
event: &M::Event,
policy: &P,
dispatch: F,
) -> Result<(), LifecycleError<M::Mode>>
pub fn feed_checked_and_dispatch<P, F>( &mut self, event: &M::Event, policy: &P, dispatch: F, ) -> Result<(), LifecycleError<M::Mode>>
Like Runner::feed_and_dispatch, but checks policy before applying a mode change.
§Errors
Returns LifecycleError::TransitionDenied if the policy rejects the transition.