Skip to main content

nautilus_live/node/
state.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::sync::{
17    Arc,
18    atomic::{AtomicU8, Ordering},
19};
20
21use super::metrics::{RunnerMetrics, RunnerMetricsSnapshot};
22
23const STOP_REQUESTED: u8 = 1 << 7;
24const STATE_MASK: u8 = !STOP_REQUESTED;
25
26/// Lifecycle state of the `LiveNode` runner.
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
28#[repr(u8)]
29#[cfg_attr(
30    feature = "python",
31    pyo3::pyclass(
32        frozen,
33        eq,
34        eq_int,
35        module = "nautilus_trader.live",
36        from_py_object,
37        rename_all = "SCREAMING_SNAKE_CASE",
38    )
39)]
40#[cfg_attr(
41    feature = "python",
42    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.live")
43)]
44pub enum NodeState {
45    #[default]
46    Idle = 0,
47    Starting = 1,
48    Running = 2,
49    ShuttingDown = 3,
50    Stopped = 4,
51}
52
53impl NodeState {
54    /// Creates a `NodeState` from its `u8` representation.
55    ///
56    /// # Panics
57    ///
58    /// Panics if the value is not a valid `NodeState` discriminant (0-4).
59    #[must_use]
60    pub const fn from_u8(value: u8) -> Self {
61        match value {
62            0 => Self::Idle,
63            1 => Self::Starting,
64            2 => Self::Running,
65            3 => Self::ShuttingDown,
66            4 => Self::Stopped,
67            _ => panic!("Invalid NodeState value"),
68        }
69    }
70
71    /// Returns the `u8` representation of this state.
72    #[must_use]
73    pub const fn as_u8(self) -> u8 {
74        self as u8
75    }
76
77    /// Returns whether the state is `Running`.
78    #[must_use]
79    pub const fn is_running(&self) -> bool {
80        matches!(self, Self::Running)
81    }
82}
83
84/// Determines which lifecycle responsibilities the node owns while running.
85///
86/// Both modes run the same event loop. The mode only decides whether the node installs process
87/// signal handlers, which a host application must own for itself.
88#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
89pub enum NodeRunMode {
90    /// The node owns the thread it runs on and installs its own signal handlers.
91    #[default]
92    Owned,
93    /// A host event loop drives the node, and the host owns signal handling and shutdown.
94    Hosted,
95}
96
97impl NodeRunMode {
98    /// Returns whether the node installs process signal handlers in this mode.
99    #[must_use]
100    pub const fn owns_signals(self) -> bool {
101        matches!(self, Self::Owned)
102    }
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub(super) enum RunningTransition {
107    Entered,
108    StopRequested,
109    Invalid(u8),
110}
111
112/// A thread-safe handle to control a `LiveNode` from other threads.
113///
114/// This allows stopping and querying the node's state without requiring the
115/// node itself to be Send + Sync.
116#[derive(Clone, Debug)]
117pub struct LiveNodeHandle {
118    control: Arc<AtomicU8>,
119    pub(crate) metrics: Arc<RunnerMetrics>,
120}
121
122impl Default for LiveNodeHandle {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128impl LiveNodeHandle {
129    /// Creates a new handle with default (`Idle`) state.
130    #[must_use]
131    pub fn new() -> Self {
132        Self {
133            control: Arc::new(AtomicU8::new(NodeState::Idle.as_u8())),
134            metrics: Arc::new(RunnerMetrics::default()),
135        }
136    }
137
138    pub(crate) fn set_starting(&self) {
139        self.set_state(NodeState::Starting);
140    }
141
142    pub(crate) fn set_shutting_down(&self) {
143        self.set_state(NodeState::ShuttingDown);
144    }
145
146    pub(crate) fn set_stopped(&self) {
147        self.set_state(NodeState::Stopped);
148    }
149
150    pub(super) fn try_set_running(&self) -> RunningTransition {
151        match self.control.compare_exchange(
152            NodeState::Starting.as_u8(),
153            NodeState::Running.as_u8(),
154            Ordering::AcqRel,
155            Ordering::Acquire,
156        ) {
157            Ok(_) => RunningTransition::Entered,
158            Err(control) if control == (NodeState::Starting.as_u8() | STOP_REQUESTED) => {
159                RunningTransition::StopRequested
160            }
161            Err(control) => RunningTransition::Invalid(control),
162        }
163    }
164
165    fn set_state(&self, state: NodeState) {
166        let _ = self
167            .control
168            .try_update(Ordering::AcqRel, Ordering::Acquire, |control| {
169                Some((control & STOP_REQUESTED) | state.as_u8())
170            });
171    }
172
173    /// Returns the current node state.
174    #[must_use]
175    pub fn state(&self) -> NodeState {
176        NodeState::from_u8(self.control.load(Ordering::Acquire) & STATE_MASK)
177    }
178
179    /// Returns whether the node should stop.
180    #[must_use]
181    pub fn should_stop(&self) -> bool {
182        self.control.load(Ordering::Acquire) & STOP_REQUESTED != 0
183    }
184
185    /// Returns whether the node is currently running.
186    #[must_use]
187    pub fn is_running(&self) -> bool {
188        self.state().is_running()
189    }
190
191    /// Returns a by-value snapshot of `LiveNode::run` dispatch metrics after startup.
192    #[must_use]
193    pub fn metrics_snapshot(&self) -> RunnerMetricsSnapshot {
194        self.metrics.snapshot()
195    }
196
197    /// Signals the node to stop.
198    pub fn stop(&self) {
199        self.control.fetch_or(STOP_REQUESTED, Ordering::AcqRel);
200    }
201}
202
203#[derive(Clone, Copy, Debug, PartialEq, Eq)]
204pub(super) enum EngineConnectionStatus {
205    Connected,
206    TimedOut,
207    StopRequested,
208    ShutdownRequested,
209}
210
211impl EngineConnectionStatus {
212    pub(super) const fn abort_reason(self) -> Option<&'static str> {
213        match self {
214            Self::Connected | Self::TimedOut => None,
215            Self::StopRequested => Some("Stop signal received during startup"),
216            Self::ShutdownRequested => Some("Shutdown signal received during startup"),
217        }
218    }
219}