venus_core/execute/
reload.rs

1//! Hot reload support for Venus notebooks.
2//!
3//! Handles the safe unloading and reloading of cell libraries while
4//! preserving state across recompilations.
5
6use std::collections::HashMap;
7
8use crate::compile::{
9    CellCompiler, CompilationResult, CompiledCell, CompilerConfig, ToolchainManager,
10};
11use crate::error::{Error, Result};
12use crate::graph::{CellId, CellInfo, GraphEngine};
13
14use super::LinearExecutor;
15use super::context::CellContext;
16
17/// Manages hot reloading of cells.
18///
19/// Coordinates the process of:
20/// 1. Saving cell state
21/// 2. Unloading the old library
22/// 3. Compiling the new version
23/// 4. Loading the new library
24/// 5. Restoring state (if compatible)
25pub struct HotReloader {
26    /// Compiler for recompiling cells
27    compiler: CellCompiler,
28    /// Active cell contexts for cleanup
29    contexts: HashMap<CellId, CellContext>,
30}
31
32impl HotReloader {
33    /// Create a new hot reloader.
34    pub fn new(config: CompilerConfig) -> Result<Self> {
35        let toolchain = ToolchainManager::new()?;
36        let compiler = CellCompiler::new(config, toolchain);
37
38        Ok(Self {
39            compiler,
40            contexts: HashMap::new(),
41        })
42    }
43
44    /// Create with an existing compiler.
45    pub fn with_compiler(compiler: CellCompiler) -> Self {
46        Self {
47            compiler,
48            contexts: HashMap::new(),
49        }
50    }
51
52    /// Register a cell context for cleanup tracking.
53    pub fn register_context(&mut self, cell_id: CellId, context: CellContext) {
54        self.contexts.insert(cell_id, context);
55    }
56
57    /// Get a cell's context.
58    pub fn get_context(&self, cell_id: CellId) -> Option<&CellContext> {
59        self.contexts.get(&cell_id)
60    }
61
62    /// Get a mutable reference to a cell's context.
63    pub fn get_context_mut(&mut self, cell_id: CellId) -> Option<&mut CellContext> {
64        self.contexts.get_mut(&cell_id)
65    }
66
67    /// Reload a single cell.
68    ///
69    /// Returns the new compiled cell on success.
70    pub fn reload_cell(
71        &mut self,
72        executor: &mut LinearExecutor,
73        cell_info: &CellInfo,
74        deps_hash: u64,
75    ) -> Result<CompiledCell> {
76        let cell_id = cell_info.id;
77
78        // Step 1: Abort and cleanup the old context
79        if let Some(mut ctx) = self.contexts.remove(&cell_id) {
80            tracing::info!("Cleaning up cell {:?} for reload", cell_id);
81            ctx.abort();
82        }
83
84        // Step 2: Save current output (for potential restoration)
85        let saved_output = executor.state().get_output(cell_id);
86
87        // Step 3: Unload the old library but KEEP IT for rollback
88        let old_cell = executor.unload_cell(cell_id);
89
90        // Step 4: Recompile the cell
91        tracing::info!("Recompiling cell {:?}", cell_id);
92        let result = self.compiler.compile(cell_info, deps_hash);
93
94        match result {
95            CompilationResult::Success(compiled) | CompilationResult::Cached(compiled) => {
96                // Step 5: Load the new library (old one will be dropped here)
97                let dep_count = cell_info.dependencies.len();
98                executor.load_cell(compiled.clone(), dep_count)?;
99
100                // Step 6: Register new context
101                let new_ctx = CellContext::new(cell_id, cell_info.name.clone());
102                self.contexts.insert(cell_id, new_ctx);
103
104                // Note: We don't restore the output automatically
105                // The caller should decide whether to re-execute or restore
106                // based on schema compatibility
107
108                tracing::info!("Cell {:?} reloaded successfully", cell_id);
109                Ok(compiled)
110            }
111            CompilationResult::Failed { cell_id, errors } => {
112                // Compilation failed - restore the old state
113                tracing::error!("Cell {:?} compilation failed: {:?}", cell_id, errors);
114
115                // Restore old library if we had one
116                if let Some(old_loaded_cell) = old_cell {
117                    tracing::info!("Restoring old cell {:?} after compilation failure", cell_id);
118                    executor.restore_cell(old_loaded_cell);
119                }
120
121                // Restore output if we had one
122                if let Some(output) = saved_output {
123                    executor
124                        .state_mut()
125                        .store_output(cell_id, (*output).clone());
126                }
127
128                Err(Error::Compilation {
129                    cell_id: Some(cell_id.to_string()),
130                    message: format!("{} compilation errors", errors.len()),
131                })
132            }
133        }
134    }
135
136    /// Reload multiple cells, respecting dependency order.
137    ///
138    /// Cells are reloaded in topological order to ensure dependencies
139    /// are available before dependents are executed.
140    pub fn reload_cells(
141        &mut self,
142        executor: &mut LinearExecutor,
143        cells: &[&CellInfo],
144        graph: &GraphEngine,
145    ) -> Result<Vec<CompiledCell>> {
146        // Sort by execution order (topological order)
147        let execution_order = graph.topological_order()?;
148        let mut sorted_cells: Vec<&CellInfo> = cells.to_vec();
149        sorted_cells.sort_by_key(|c| {
150            execution_order
151                .iter()
152                .position(|&id| id == c.id)
153                .unwrap_or(usize::MAX)
154        });
155
156        // Reload each cell
157        let mut compiled = Vec::new();
158        for cell in sorted_cells {
159            // Calculate deps hash from dependencies
160            let deps_hash = self.calculate_deps_hash(cell, graph);
161            let result = self.reload_cell(executor, cell, deps_hash)?;
162            compiled.push(result);
163        }
164
165        Ok(compiled)
166    }
167
168    /// Reload a cell and its downstream dependents.
169    pub fn reload_cascade(
170        &mut self,
171        executor: &mut LinearExecutor,
172        cell_info: &CellInfo,
173        graph: &GraphEngine,
174    ) -> Result<Vec<CompiledCell>> {
175        let cell_id = cell_info.id;
176
177        // Get all affected cells (the modified cell + its dependents)
178        let dependents = graph.invalidated_cells(cell_id);
179        let mut affected_ids: Vec<CellId> = vec![cell_id];
180        affected_ids.extend(dependents);
181
182        // Invalidate outputs for all affected cells
183        executor.state_mut().invalidate_many(&affected_ids);
184
185        // Reload the modified cell
186        let deps_hash = self.calculate_deps_hash(cell_info, graph);
187        let compiled = self.reload_cell(executor, cell_info, deps_hash)?;
188
189        // Note: Dependents don't need recompilation, just re-execution
190        // since their source didn't change
191
192        Ok(vec![compiled])
193    }
194
195    /// Calculate dependency hash for a cell.
196    fn calculate_deps_hash(&self, cell: &CellInfo, _graph: &GraphEngine) -> u64 {
197        use std::collections::hash_map::DefaultHasher;
198        use std::hash::{Hash, Hasher};
199
200        let mut hasher = DefaultHasher::new();
201
202        // Hash all dependency cell names (which determines their outputs)
203        for dep in &cell.dependencies {
204            dep.param_name.hash(&mut hasher);
205            dep.param_type.hash(&mut hasher);
206        }
207
208        // Could also hash the dependency cells' source hashes for more precision
209        // but this is sufficient for most cases
210
211        hasher.finish()
212    }
213
214    /// Abort all active cell contexts.
215    pub fn abort_all(&mut self) {
216        for (cell_id, mut ctx) in self.contexts.drain() {
217            tracing::info!("Aborting cell {:?}", cell_id);
218            ctx.abort();
219        }
220    }
221}
222
223impl Drop for HotReloader {
224    fn drop(&mut self) {
225        self.abort_all();
226    }
227}
228
229// Note: ReloadResult was removed as unused dead code.
230// If detailed reload status is needed in the future, it can be re-added
231// with fields: reloaded: Vec<CellId>, needs_execution: Vec<CellId>, failed: Vec<(CellId, Error)>
232
233// Note: Tests for ReloadResult removed along with the unused struct.