parallax_runtime/
manager.rs1use 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
15pub struct RuntimeManager {
17 adapters: RwLock<HashMap<String, BoxRuntimeAdapter>>,
18 active: AtomicUsize,
19 max_concurrent: usize,
20}
21
22impl RuntimeManager {
23 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 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 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 }
48 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 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 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 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 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 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 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 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}