Skip to main content

parallax_runtime/
manager.rs

1//! Runtime manager coordinating adapters.
2
3use crate::adapter::BoxRuntimeAdapter;
4use crate::{CaptureOutcome, RuntimeHandle};
5use parallax_core::{
6    ErrorCode, ExecutionRequest, ExecutionResult, ExecutionState, ParallaxError, Remediation,
7    RestoreResult, RuntimeKind, RuntimeStatus,
8};
9use parallax_ir::PirDocument;
10use parallax_security::SandboxPolicy;
11use parking_lot::RwLock;
12use std::collections::HashMap;
13use std::sync::atomic::{AtomicUsize, Ordering};
14
15/// Central runtime registry and execution entrypoint.
16pub struct RuntimeManager {
17    adapters: RwLock<HashMap<String, BoxRuntimeAdapter>>,
18    active: AtomicUsize,
19    max_concurrent: usize,
20}
21
22impl RuntimeManager {
23    /// Create an empty manager.
24    pub fn new(max_concurrent: usize) -> Self {
25        Self {
26            adapters: RwLock::new(HashMap::new()),
27            active: AtomicUsize::new(0),
28            max_concurrent: max_concurrent.max(1),
29        }
30    }
31
32    /// Register an adapter (replaces existing for same kind).
33    pub fn register(&self, adapter: BoxRuntimeAdapter) {
34        let key = adapter.kind().as_str().to_string();
35        self.adapters.write().insert(key, adapter);
36    }
37
38    /// List registered runtimes with live probe status.
39    pub async fn list(&self) -> Vec<RuntimeHandle> {
40        let adapters: Vec<BoxRuntimeAdapter> = self.adapters.read().values().cloned().collect();
41        let mut out = Vec::new();
42        for a in adapters {
43            let status = a.probe().await;
44            let mut metadata = a.metadata();
45            if let RuntimeStatus::Ready = &status {
46                // keep host_version if adapter filled it during probe via metadata — adapters set on construct
47            }
48            // Prefer status-derived version details left in metadata by adapters.
49            let _ = &mut metadata;
50            out.push(RuntimeHandle {
51                kind: a.kind(),
52                metadata: a.metadata(),
53                capabilities: a.capabilities(),
54                status,
55            });
56        }
57        out.sort_by(|a, b| a.kind.as_str().cmp(b.kind.as_str()));
58        out
59    }
60
61    /// Get adapter by kind.
62    pub fn get(&self, kind: &RuntimeKind) -> parallax_core::Result<BoxRuntimeAdapter> {
63        self.adapters
64            .read()
65            .get(kind.as_str())
66            .cloned()
67            .ok_or_else(|| {
68                ParallaxError::new(
69                    ErrorCode::RuntimeUnavailable,
70                    format!("no adapter registered for {kind}"),
71                )
72                .with_runtime(kind.clone())
73                .remediate(Remediation::new(
74                    "Register the adapter or check installation",
75                ))
76            })
77    }
78
79    async fn with_slot<F, T>(&self, f: F) -> parallax_core::Result<T>
80    where
81        F: std::future::Future<Output = parallax_core::Result<T>>,
82    {
83        loop {
84            let cur = self.active.load(Ordering::SeqCst);
85            if cur >= self.max_concurrent {
86                return Err(ParallaxError::new(
87                    ErrorCode::ResourceLimitExceeded,
88                    format!("bounded concurrency exceeded (max {})", self.max_concurrent),
89                )
90                .with_source("parallax-runtime")
91                .with_operation("with_slot"));
92            }
93            if self
94                .active
95                .compare_exchange(cur, cur + 1, Ordering::SeqCst, Ordering::SeqCst)
96                .is_ok()
97            {
98                break;
99            }
100        }
101        // Release the concurrency slot even if the future panics.
102        struct ActiveGuard<'a>(&'a AtomicUsize);
103        impl Drop for ActiveGuard<'_> {
104            fn drop(&mut self) {
105                self.0.fetch_sub(1, Ordering::SeqCst);
106            }
107        }
108        let _guard = ActiveGuard(&self.active);
109        f.await
110    }
111
112    /// Execute via the appropriate adapter.
113    pub async fn execute(
114        &self,
115        request: ExecutionRequest,
116        policy: &SandboxPolicy,
117    ) -> parallax_core::Result<ExecutionResult> {
118        let adapter = self.get(&request.runtime)?;
119        parallax_core::check_adapter_interface(adapter.metadata().interface_version)?;
120        adapter
121            .capabilities()
122            .require("execution", &request.runtime)?;
123        if request.capture_state {
124            adapter
125                .capabilities()
126                .require("globals", &request.runtime)?;
127            adapter.capabilities().require("values", &request.runtime)?;
128        }
129        self.with_slot(adapter.execute(request, policy)).await
130    }
131
132    /// Execute and build PIR from captured bindings.
133    pub async fn execute_and_capture(
134        &self,
135        mut request: ExecutionRequest,
136        capture_names: &[String],
137        policy: &SandboxPolicy,
138    ) -> parallax_core::Result<CaptureOutcome> {
139        request.capture_state = true;
140        // Encode capture names into program if using File/Inline — adapters read request via protocol.
141        // For CaptureBindings variant, names already present.
142        use parallax_core::execution::ProgramSource;
143        if !capture_names.is_empty() {
144            match &request.program {
145                ProgramSource::File { path } => {
146                    let source = std::fs::read_to_string(path).map_err(|e| {
147                        ParallaxError::new(ErrorCode::Io, e.to_string())
148                            .with_operation("read_source")
149                    })?;
150                    request.program = ProgramSource::CaptureBindings {
151                        source,
152                        names: capture_names.to_vec(),
153                        filename: path.clone(),
154                    };
155                }
156                ProgramSource::Inline { source, filename } => {
157                    request.program = ProgramSource::CaptureBindings {
158                        source: source.clone(),
159                        names: capture_names.to_vec(),
160                        filename: filename.clone(),
161                    };
162                }
163                ProgramSource::CaptureBindings { .. } => {}
164                ProgramSource::Bytes { .. } => {}
165            }
166        }
167
168        let execution = self.execute(request.clone(), policy).await?;
169        let pir = if let Some(state) = &execution.state {
170            // Prefer heap as bindings object if present.
171            if state.heap.is_object() {
172                PirDocument::from_bindings_json(&state.heap)?
173            } else {
174                PirDocument::new()
175            }
176        } else {
177            PirDocument::new()
178        };
179        let state = execution.state.clone().unwrap_or_else(|| {
180            ExecutionState::empty(
181                request.runtime.clone(),
182                DefaultCapabilities::for_kind(&request.runtime),
183            )
184        });
185        Ok(CaptureOutcome {
186            execution,
187            pir,
188            state,
189        })
190    }
191
192    /// Restore bindings.
193    pub async fn restore_bindings(
194        &self,
195        runtime: RuntimeKind,
196        pir: &PirDocument,
197        policy: &SandboxPolicy,
198    ) -> parallax_core::Result<RestoreResult> {
199        let adapter = self.get(&runtime)?;
200        parallax_core::check_adapter_interface(adapter.metadata().interface_version)?;
201        adapter.capabilities().require("globals", &runtime)?;
202        adapter.capabilities().require("values", &runtime)?;
203        pir.validate()?;
204        self.with_slot(adapter.restore(pir, policy)).await
205    }
206}
207
208struct DefaultCapabilities;
209impl DefaultCapabilities {
210    fn for_kind(kind: &RuntimeKind) -> parallax_core::RuntimeCapabilities {
211        match kind {
212            RuntimeKind::Python => parallax_core::RuntimeCapabilities::python(),
213            RuntimeKind::JavaScript => parallax_core::RuntimeCapabilities::javascript(),
214            RuntimeKind::Wasm => parallax_core::RuntimeCapabilities::wasm(),
215            RuntimeKind::Other(_) => parallax_core::RuntimeCapabilities::none(),
216        }
217    }
218}
219
220impl Default for RuntimeManager {
221    fn default() -> Self {
222        Self::new(4)
223    }
224}