Skip to main content

fxcp_core/consistency/
serialization.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2// Copyright (C) 2025 Joel Wirāmu Pauling <aenertia@aenertia.net>
3//
4// fxcp-core/src/consistency/serialization.rs  --  Binary serialization for WAL entries
5
6//! Serialization helpers for WAL entry persistence.
7
8use std::collections::HashMap;
9use std::sync::Arc;
10use crate::consistency::sequencer::{GlobalSequencer, SequenceBarrier};
11use std::path::PathBuf;
12use tracing::{debug, warn};
13use tokio::sync::Notify;
14
15/// Classification of serialized operations for WAL conflict detection.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum OpKind {
18    Write,
19    Rename,
20}
21
22/// RAII guard that completes a serialized inode operation on drop.
23pub struct OpGuard {
24    engine: Arc<SerializationEngine>,
25    inode: u64,
26    ticket: u64,
27}
28
29impl Drop for OpGuard {
30    fn drop(&mut self) {
31        self.engine.complete_op(self.inode, self.ticket);
32    }
33}
34
35/// RAII guard that releases a path-level lock on drop.
36pub struct PathGuard {
37    engine: Arc<SerializationEngine>,
38    path: PathBuf,
39}
40
41impl Drop for PathGuard {
42    fn drop(&mut self) {
43        self.engine.release_path(&self.path);
44    }
45}
46
47struct InodeState {
48    sequencer: GlobalSequencer,
49    barrier: SequenceBarrier,
50}
51
52impl InodeState {
53    fn new() -> Self {
54        Self {
55            sequencer: GlobalSequencer::new(0),
56            barrier: SequenceBarrier::new(0),
57        }
58    }
59}
60
61/// Ticket-based serialization engine ensuring strict per-inode operation ordering.
62pub struct SerializationEngine {
63    state: std::sync::Mutex<HashMap<u64, Arc<InodeState>>>,
64    path_locks: std::sync::Mutex<HashMap<PathBuf, Arc<Notify>>>,
65}
66
67impl SerializationEngine {
68    /// Creates a new serialization engine wrapped in an `Arc`.
69    pub fn new() -> Arc<Self> {
70        Arc::new(Self {
71            state: std::sync::Mutex::new(HashMap::new()),
72            path_locks: std::sync::Mutex::new(HashMap::new()),
73        })
74    }
75
76    /// Returns whether any operation is in-flight for the given inode.
77    pub fn is_active(&self, inode: u64) -> bool {
78        let state = self.state.lock().unwrap_or_else(|poisoned| {
79            warn!("Mutex poisoned in SerializationEngine::is_active, recovering: {}", poisoned);
80            poisoned.into_inner()
81        });
82        state.contains_key(&inode)
83    }
84
85    /// Acquires a ticket-based barrier for an inode operation.
86    /// This ensures strict serialization of operations on the same inode
87    /// based on the order they arrive, preventing deadlocks.
88    pub async fn acquire_barrier(self: &Arc<Self>, inode: u64, _kind: OpKind) -> Result<OpGuard, std::io::Error> {
89        // 1. Acquire Ticket
90        let (inode_state, ticket) = {
91            let mut map = self.state.lock().unwrap_or_else(|poisoned| {
92                warn!("Mutex poisoned in SerializationEngine::acquire_barrier, recovering: {}", poisoned);
93                poisoned.into_inner()
94            });
95            let state = map.entry(inode).or_insert_with(|| Arc::new(InodeState::new())).clone();
96            
97            // Get the next ticket. 
98            // Note: In this architecture, we treat Writes and Renames with the same 
99            // strict ordering requirement for simplicity and deadlock prevention.
100            // Writer A (100) blocks Writer B (101).
101            let ticket = state.sequencer.next();
102            
103            (state, ticket)
104        };
105
106        // 2. Wait for previous operation to complete
107        // Since tickets are 1-based, ticket 1 waits for 0 (completed by default).
108        // Ticket 101 waits for 100.
109        let dependency = ticket - 1;
110        
111        if dependency > 0 {
112            // Wait for the barrier to reach the dependency state.
113            // This is deadlock-free because the dependency is strictly lower than our ticket.
114            inode_state.barrier.wait_for(dependency).await;
115        }
116
117        Ok(OpGuard {
118            engine: self.clone(),
119            inode,
120            ticket,
121        })
122    }
123
124    /// Acquires an exclusive path-level lock, waiting if another operation holds it.
125    pub async fn acquire_path_barrier(self: &Arc<Self>, path: &PathBuf) -> Result<PathGuard, std::io::Error> {
126        loop {
127            let wait_notify = {
128                let mut locks = self.path_locks.lock().unwrap_or_else(|poisoned| {
129                    warn!("Mutex poisoned in SerializationEngine::acquire_path_barrier, recovering: {}", poisoned);
130                    poisoned.into_inner()
131                });
132                if let Some(notify) = locks.get(path) {
133                    Some(notify.clone())
134                } else {
135                    locks.insert(path.clone(), Arc::new(Notify::new()));
136                    None
137                }
138            };
139
140            if let Some(notify) = wait_notify {
141                notify.notified().await;
142            } else {
143                return Ok(PathGuard {
144                    engine: self.clone(),
145                    path: path.clone(),
146                });
147            }
148        }
149    }
150
151    fn release_path(&self, path: &PathBuf) {
152        let mut locks = self.path_locks.lock().unwrap_or_else(|poisoned| {
153            warn!("Mutex poisoned in SerializationEngine::release_path, recovering: {}", poisoned);
154            poisoned.into_inner()
155        });
156        if let Some(notify) = locks.remove(path) {
157            notify.notify_waiters();
158        }
159    }
160
161    /// Legacy sequence checking helpers - now no-ops or simple pass-throughs
162    /// as the ticket barrier handles ordering implicitly.
163    /// Legacy no-op: sequence checking is now handled by the ticket barrier.
164    pub fn check_sequence(&self, _inode: u64, _seq: u64) -> bool {
165        true 
166    }
167
168    /// Legacy no-op: sequence updates are managed internally by the sequencer.
169    pub fn update_sequence(&self, _inode: u64, _seq: u64) {
170        // Managed internally by sequencer
171    }
172
173    fn complete_op(&self, inode: u64, ticket: u64) {
174        // We need to access the barrier to mark completion.
175        let state_opt = {
176            let map = self.state.lock().unwrap_or_else(|poisoned| {
177                warn!("Mutex poisoned in SerializationEngine::complete_op, recovering: {}", poisoned);
178                poisoned.into_inner()
179            });
180            map.get(&inode).cloned()
181        };
182
183        if let Some(state) = state_opt {
184            debug!("Serialization: Completed ticket {} for inode {}", ticket, inode);
185            state.barrier.complete(ticket);
186            
187            // Note: We don't remove the InodeState from the map aggressively here.
188            // In a long-running system, we might want a cleanup task to remove 
189            // InodeStates where barrier.current() == sequencer.current() and no activity.
190            // For now, we rely on LRU or system restart to clean up map entries if they grow too large,
191            // or the memory overhead is considered acceptable for active inodes.
192        } else {
193            warn!("Serialization: Attempted to complete op for unknown inode {}", inode);
194        }
195    }
196}