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}