aimx/values/node.rs
1//! Workflow node management with lazy loading and thread-safe access
2//!
3//! This module provides the [`Node`] struct, which represents a clonable handle to a workflow
4//! with lazy loading semantics and concurrency-aware access patterns.
5//!
6//! # Overview
7//!
8//! The `Node` struct provides an efficient way to manage workflow access in concurrent
9//! environments. It implements lazy loading, where workflows are only loaded from disk
10//! when first accessed, and uses atomic reference counting for efficient cloning.
11//!
12//! # Concurrency Model
13//!
14//! `Node` is designed for concurrent access:
15//! - Read access: Multiple threads can concurrently read the workflow
16//! - Write access: Mutation requires external coordination through the lock manager
17//! - Lazy loading: Thread-safe transitions from unloaded to loaded state
18//!
19//! # Examples
20//!
21//! ```rust
22//! # use aimx::Reference;
23//! # use aimx::values::Node;
24//! let reference = Reference::new("example");
25//! let node = Node::new(reference);
26//!
27//! // Node is created in unloaded state
28//! // Workflow loading happens lazily on first access
29//! ```
30
31use std::{
32 sync::{Arc, RwLock},
33};
34use crate::{
35 Reference,
36 Workflow,
37 WorkflowLike,
38};
39
40/// Internal state management for workflow nodes
41///
42/// This enum represents the lifecycle state of a workflow node's in-memory representation.
43/// Nodes start in the `Unloaded` state and transition to `Loaded` upon first access.
44///
45/// # State Transitions
46///
47/// ```text
48/// Unloaded { reference } ────load────▶ Loaded { workflow }
49/// ```
50#[derive(Debug, Clone)]
51enum NodeState {
52 /// Workflow is not currently loaded in memory
53 ///
54 /// Contains a [`Reference`] that can be used to load the workflow from disk
55 /// when needed. This is the initial state of all nodes.
56 Unloaded { reference: Reference },
57 /// Workflow is loaded and available as a shared, immutable snapshot
58 ///
59 /// The workflow is wrapped in an [`Arc`] to allow multiple threads to
60 /// access the same immutable snapshot concurrently. This state is reached
61 /// after the first call to [`Node::get_workflow`] or [`Node::get_workflow_mut`].
62 Loaded { workflow: Arc<Workflow> },
63}
64
65/// A clonable, concurrency-aware handle to a single workflow
66///
67/// `Node` provides lazy loading semantics where workflows are only loaded from disk
68/// when first accessed. It implements efficient cloning through [`Arc`] sharing,
69/// making it suitable for concurrent access patterns.
70///
71/// # Concurrency Model
72///
73/// `Node` is designed for concurrent access:
74/// - **Read access**: Multiple threads can concurrently read the workflow using
75/// [`get_workflow()`](Node::get_workflow) or [`get_workflow_like()`](Node::get_workflow_like)
76/// - **Write access**: Mutation requires external coordination through the lock manager
77/// and should be done through [`get_workflow_mut()`](Node::get_workflow_mut)
78/// - **Lazy loading**: Thread-safe transitions from unloaded to loaded state using
79/// double-checked locking pattern
80///
81/// # Lifecycle
82///
83/// Nodes begin in the `Unloaded` state and transition to
84/// `Loaded` on first access:
85///
86/// ```text
87/// Node::new() ──▶ Unloaded ──get_workflow()──▶ Loaded ──set_workflow()──▶ Loaded
88/// ```
89///
90/// # Examples
91///
92/// Basic usage:
93///
94/// ```rust
95/// # use aimx::Reference;
96/// # use aimx::values::Node;
97/// let reference = Reference::new("example");
98/// let node = Node::new(reference);
99///
100/// // Node is created in unloaded state
101/// // Workflow loading happens lazily on first access
102/// ```
103///
104/// Cloning for concurrent access:
105///
106/// ```rust
107/// # use aimx::Reference;
108/// # use aimx::values::Node;
109/// # let reference = Reference::new("example");
110/// let node1 = Node::new(reference);
111/// let node2 = node1.clone();
112///
113/// // Both nodes share the same internal state
114/// assert_eq!(node1, node2);
115/// ```
116///
117/// The `Clone` implementation is cheap, as it only increments the reference
118/// count of the internal [`Arc`].
119#[derive(Debug, Clone)]
120pub struct Node {
121 /// Internal state protected by read-write lock
122 inner: Arc<RwLock<NodeState>>,
123}
124
125impl Node {
126 /// Creates a new Node in an `Unloaded` state
127 ///
128 /// The workflow will remain unloaded until the first access through
129 /// [`get_workflow()`](Node::get_workflow) or [`get_workflow_mut()`](Node::get_workflow_mut).
130 ///
131 /// # Arguments
132 /// * `reference` - The reference pointing to the workflow file location
133 ///
134 /// # Examples
135 /// ```rust
136 /// use aimx::Reference;
137 /// use aimx::values::Node;
138 ///
139 /// let reference = Reference::new("main");
140 /// let node = Node::new(reference);
141 /// ```
142 pub fn new(reference: Reference) -> Self {
143 Node {
144 inner: Arc::new(RwLock::new(NodeState::Unloaded {
145 reference: reference,
146 })),
147 }
148 }
149
150 /// Gets the workflow for read-only access, loading it from disk if necessary
151 ///
152 /// This method employs a double-checked locking pattern to ensure thread safety:
153 /// 1. Acquires read lock to check if workflow is already loaded
154 /// 2. If unloaded, releases read lock and acquires write lock
155 /// 3. Re-checks state (protected by write lock) and loads workflow if still needed
156 ///
157 /// This ensures that:
158 /// - Concurrent readers never block each other when workflow is loaded
159 /// - Only one thread loads the workflow when transitioning from unloaded to loaded
160 /// - No race conditions during state transition
161 ///
162 /// # Returns
163 /// An [`Arc<Workflow>`] containing a shared, immutable reference to the workflow
164 ///
165 /// # Panics
166 ///
167 /// This method will panic if the thread is unable to acquire the necessary locks,
168 /// which typically indicates a deadlock scenario.
169 pub fn get_workflow(&self) -> Arc<Workflow> {
170 // Acquire read lock first
171 let read_guard = self.inner.read().unwrap();
172
173 match &*read_guard {
174 NodeState::Loaded { workflow } => {
175 return workflow.clone();
176 }
177 NodeState::Unloaded { reference: _ } => {
178 // Need to load, so release read lock and acquire write lock
179 drop(read_guard);
180 let mut write_guard = self.inner.write().unwrap();
181
182 // Double-check after acquiring write lock
183 match &*write_guard {
184 NodeState::Loaded { workflow } => {
185 return workflow.clone();
186 }
187 NodeState::Unloaded { reference } => {
188 // Load the workflow
189 let workflow = Workflow::load_new(reference);
190 let workflow_arc = Arc::new(workflow);
191
192 // Update state
193 *write_guard = NodeState::Loaded { workflow: workflow_arc.clone() };
194
195 return workflow_arc;
196 }
197 }
198 }
199 }
200 }
201
202 /// Gets the workflow as a trait object implementing [`WorkflowLike`]
203 ///
204 /// This provides dynamic dispatch capabilities for workflow operations.
205 /// The workflow is loaded from disk if not already in memory.
206 ///
207 /// This method is equivalent to calling [`get_workflow()`](Node::get_workflow) and
208 /// casting the result to `Arc<dyn WorkflowLike>`. It's provided as a convenience
209 /// for cases where you need to work with the workflow through its trait interface.
210 ///
211 /// # Returns
212 /// An [`Arc<dyn WorkflowLike>`] trait object for dynamic workflow operations
213 ///
214 /// # Panics
215 ///
216 /// This method will panic if the thread is unable to acquire the necessary locks,
217 /// which typically indicates a deadlock scenario.
218 pub fn get_workflow_like(&self) -> Arc<dyn WorkflowLike> {
219 self.get_workflow() as Arc<dyn WorkflowLike>
220 }
221
222 /// Gets the workflow for mutable access, loading it from disk if necessary
223 ///
224 /// # Important Safety Requirement
225 /// The caller **must** already have write permission from the lock manager
226 /// before calling this method. This method does not acquire external locks.
227 ///
228 /// This method follows the same double-checked locking pattern as
229 /// [`get_workflow()`](Node::get_workflow) but returns a cloned [`Workflow`]
230 /// suitable for mutation rather than a shared reference.
231 ///
232 /// Unlike [`get_workflow()`](Node::get_workflow) which returns a shared reference,
233 /// this method returns an owned [`Workflow`] that can be modified. The internal
234 /// state still remains loaded, but modifications to the returned workflow won't
235 /// affect the node's state until [`set_workflow()`](Node::set_workflow) is called.
236 ///
237 /// # Returns
238 /// A cloned [`Workflow`] instance that can be modified
239 ///
240 /// # Panics
241 ///
242 /// This method will panic if the thread is unable to acquire the necessary locks,
243 /// which typically indicates a deadlock scenario.
244 pub fn get_workflow_mut(&self) -> Workflow {
245 // Acquire read lock first
246 let read_guard = self.inner.read().unwrap();
247
248 match &*read_guard {
249 NodeState::Loaded { workflow } => {
250 return (**workflow).clone();
251 }
252 NodeState::Unloaded { reference: _ } => {
253 // Need to load, so release read lock and acquire write lock
254 drop(read_guard);
255 let mut write_guard = self.inner.write().unwrap();
256
257 // Double-check after acquiring write lock
258 match &*write_guard {
259 NodeState::Loaded { workflow } => {
260 return (**workflow).clone();
261 }
262 NodeState::Unloaded { reference } => {
263 // Load the workflow
264 let workflow = Workflow::load_new(reference);
265 let workflow_arc = Arc::new(workflow.clone());
266
267 // Update state
268 *write_guard = NodeState::Loaded { workflow: workflow_arc };
269
270 return workflow;
271 }
272 }
273 }
274 }
275 }
276
277 /// Atomically replaces the workflow of the node with a concrete Workflow
278 ///
279 /// This method atomically updates the node's state to contain the provided
280 /// workflow. The previous state (loaded or unloaded) is replaced entirely.
281 ///
282 /// This is typically used after modifying a workflow obtained through
283 /// [`get_workflow_mut()`](Node::get_workflow_mut) to commit the changes back to the node.
284 ///
285 /// # Arguments
286 /// * `workflow` - The new workflow to store in the node
287 ///
288 /// # Panics
289 ///
290 /// This method will panic if the thread is unable to acquire the necessary write lock,
291 /// which typically indicates a deadlock scenario.
292 pub fn set_workflow(&self, workflow: Workflow) {
293 let mut write_guard = self.inner.write().unwrap();
294 *write_guard = NodeState::Loaded { workflow: Arc::new(workflow) };
295 }
296}
297
298impl PartialEq for Node {
299 /// Compares two `Node` instances for equality based on shared identity
300 ///
301 /// Two `Node` instances are considered equal if they reference the exact
302 /// same internal `Arc<RwLock<NodeState>>`. This means they are clones of
303 /// each other or reference the same underlying workflow state.
304 ///
305 /// This is **not** a deep comparison of workflow contents. Two nodes with
306 /// different `Arc` instances but identical workflow contents will be considered
307 /// different, while two cloned nodes sharing the same `Arc` will be equal.
308 ///
309 /// # Returns
310 /// `true` if both nodes share the same internal state pointer, `false` otherwise
311 ///
312 /// # Examples
313 ///
314 /// ```rust
315 /// # use aimx::Reference;
316 /// # use aimx::values::Node;
317 /// let reference = Reference::new("example");
318 /// let node1 = Node::new(reference);
319 /// let node2 = node1.clone();
320 ///
321 /// // Cloned nodes are equal because they share the same internal state
322 /// assert_eq!(node1, node2);
323 /// ```
324 fn eq(&self, other: &Self) -> bool {
325 Arc::ptr_eq(&self.inner, &other.inner)
326 }
327}