aardvark_core/persistent/
isolate.rs

1use std::sync::Arc;
2
3use crate::bundle::BundleFingerprint;
4use crate::config::PyRuntimeConfig;
5use crate::error::Result;
6use crate::invocation::InvocationDescriptor;
7use crate::persistent::{BundleArtifact, InlinePythonOptions};
8use crate::runtime::PyRuntime;
9use crate::runtime_language::RuntimeLanguage;
10use crate::strategy::{
11    DefaultInvocationStrategy, JsonInvocationStrategy, PyInvocationStrategy, RawCtxInput,
12    RawCtxInvocationStrategy,
13};
14use serde_json::Value as JsonValue;
15
16/// Cleanup behaviour applied after each invocation.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum CleanupMode {
19    Full,
20    SharedBuffersOnly,
21    None,
22}
23
24impl Default for CleanupMode {
25    fn default() -> Self {
26        Self::Full
27    }
28}
29
30/// Configuration applied when constructing a [`PythonIsolate`].
31#[derive(Clone)]
32pub struct IsolateConfig {
33    pub runtime: PyRuntimeConfig,
34    pub cleanup: CleanupMode,
35}
36
37impl Default for IsolateConfig {
38    fn default() -> Self {
39        Self {
40            runtime: PyRuntimeConfig::default(),
41            cleanup: CleanupMode::Full,
42        }
43    }
44}
45
46/// Handle referencing a loaded bundle artifact.
47#[derive(Clone)]
48pub struct BundleHandle {
49    artifact: Arc<BundleArtifact>,
50}
51
52impl BundleHandle {
53    /// Creates a bundle handle from a shared artifact.
54    pub fn from_artifact(artifact: Arc<BundleArtifact>) -> Self {
55        Self { artifact }
56    }
57
58    /// Returns a session prepared from the bundle's descriptor template.
59    pub fn prepare_default_handler(&self) -> HandlerSession {
60        self.prepare_handler(None)
61    }
62
63    /// Builds a handler session using an optional descriptor override.
64    pub fn prepare_handler(&self, descriptor: Option<InvocationDescriptor>) -> HandlerSession {
65        let mut descriptor = descriptor.unwrap_or_else(|| self.artifact.default_descriptor());
66        descriptor.language = descriptor.language.or(Some(self.artifact.language()));
67        HandlerSession {
68            artifact: self.artifact.clone(),
69            descriptor,
70        }
71    }
72
73    pub(crate) fn artifact(&self) -> &Arc<BundleArtifact> {
74        &self.artifact
75    }
76}
77
78/// Prepared handler ready for repeated invocation.
79pub struct HandlerSession {
80    artifact: Arc<BundleArtifact>,
81    descriptor: InvocationDescriptor,
82}
83
84impl HandlerSession {
85    /// Returns the invocation descriptor for inspection.
86    pub fn descriptor(&self) -> &InvocationDescriptor {
87        &self.descriptor
88    }
89
90    /// Provides mutable access to the underlying descriptor for fine-grained changes.
91    pub fn descriptor_mut(&mut self) -> &mut InvocationDescriptor {
92        &mut self.descriptor
93    }
94
95    /// Executes the handler using the default invocation strategy.
96    pub fn invoke(&self, isolate: &mut PythonIsolate) -> Result<crate::ExecutionOutcome> {
97        let mut strategy = DefaultInvocationStrategy;
98        isolate.invoke_with_strategy(self, &mut strategy)
99    }
100
101    /// Executes the handler using JSON adapters.
102    pub fn invoke_json(
103        &self,
104        isolate: &mut PythonIsolate,
105        input: Option<JsonValue>,
106    ) -> Result<crate::ExecutionOutcome> {
107        let mut strategy = JsonInvocationStrategy::new(input);
108        isolate.invoke_with_strategy(self, &mut strategy)
109    }
110
111    /// Executes the handler using RawCtx adapters.
112    pub fn invoke_rawctx(
113        &self,
114        isolate: &mut PythonIsolate,
115        inputs: Vec<RawCtxInput>,
116    ) -> Result<crate::ExecutionOutcome> {
117        let mut strategy = RawCtxInvocationStrategy::new(inputs);
118        isolate.invoke_with_strategy(self, &mut strategy)
119    }
120
121    /// Executes the handler asynchronously using the default strategy.
122    pub async fn invoke_async(
123        &self,
124        isolate: &mut PythonIsolate,
125    ) -> Result<crate::ExecutionOutcome> {
126        self.invoke(isolate)
127    }
128
129    pub(crate) fn artifact(&self) -> &Arc<BundleArtifact> {
130        &self.artifact
131    }
132
133    pub(crate) fn descriptor_cloned(&self) -> InvocationDescriptor {
134        self.descriptor.clone()
135    }
136}
137
138/// Persistent Pyodide isolate keeping the interpreter hot between invocations.
139pub struct PythonIsolate {
140    runtime: PyRuntime,
141    cleanup: CleanupMode,
142    loaded_fingerprint: Option<BundleFingerprint>,
143    current_artifact: Option<Arc<BundleArtifact>>,
144}
145
146impl PythonIsolate {
147    pub fn new(config: IsolateConfig) -> Result<Self> {
148        let runtime = PyRuntime::new(config.runtime.clone())?;
149        Ok(Self {
150            runtime,
151            cleanup: config.cleanup,
152            loaded_fingerprint: None,
153            current_artifact: None,
154        })
155    }
156
157    /// Ensures the isolate has materialised the bundle requested by `handle`.
158    pub fn load_bundle(&mut self, handle: &BundleHandle) -> Result<()> {
159        let fingerprint = handle.artifact().fingerprint();
160        if self.loaded_fingerprint == Some(fingerprint) {
161            self.current_artifact = Some(handle.artifact().clone());
162            return Ok(());
163        }
164        self.loaded_fingerprint = None;
165        self.current_artifact = Some(handle.artifact().clone());
166        Ok(())
167    }
168
169    /// Invokes the handler with a caller-supplied strategy.
170    pub fn invoke_with_strategy<S: PyInvocationStrategy>(
171        &mut self,
172        handler: &HandlerSession,
173        strategy: &mut S,
174    ) -> Result<crate::ExecutionOutcome> {
175        self.ensure_artifact(handler.artifact())?;
176        let bundle = handler.artifact().bundle();
177        let descriptor = handler.descriptor_cloned();
178        if handler.artifact().manifest().is_some() {
179            let (session, _) = self
180                .runtime
181                .prepare_session_with_manifest_and_descriptor(bundle, descriptor)?;
182            self.runtime.run_session_with_strategy(&session, strategy)
183        } else {
184            let session = self
185                .runtime
186                .prepare_session_with_descriptor(bundle, descriptor)?;
187            self.runtime.run_session_with_strategy(&session, strategy)
188        }
189    }
190
191    fn ensure_artifact(&mut self, artifact: &Arc<BundleArtifact>) -> Result<()> {
192        let fingerprint = artifact.fingerprint();
193        if self.loaded_fingerprint == Some(fingerprint) {
194            return Ok(());
195        }
196        self.materialise_bundle(artifact)?;
197        self.loaded_fingerprint = Some(fingerprint);
198        self.current_artifact = Some(artifact.clone());
199        Ok(())
200    }
201
202    fn materialise_bundle(&mut self, artifact: &Arc<BundleArtifact>) -> Result<()> {
203        let descriptor = artifact.default_descriptor();
204        if artifact.manifest().is_some() {
205            let bundle = artifact.bundle();
206            let (_session, _) = self
207                .runtime
208                .prepare_session_with_manifest_and_descriptor(bundle, descriptor)?;
209        } else {
210            let bundle = artifact.bundle();
211            let _ = self
212                .runtime
213                .prepare_session_with_descriptor(bundle, descriptor)?;
214        }
215        Ok(())
216    }
217
218    pub fn cleanup_mode(&self) -> CleanupMode {
219        self.cleanup
220    }
221
222    pub fn runtime_language(&self) -> Option<RuntimeLanguage> {
223        self.current_artifact
224            .as_ref()
225            .map(|artifact| artifact.language())
226    }
227
228    pub fn runtime(&mut self) -> &mut PyRuntime {
229        &mut self.runtime
230    }
231
232    /// Executes inline Python code using default manifest options and entrypoint.
233    pub fn run_inline_python(
234        &mut self,
235        code: &str,
236        entrypoint: &str,
237    ) -> Result<crate::ExecutionOutcome> {
238        let options = InlinePythonOptions {
239            entrypoint: Some(entrypoint.to_owned()),
240            ..InlinePythonOptions::default()
241        };
242        self.run_inline_python_with_options(code, options)
243    }
244
245    /// Executes inline Python code with manifest-style configuration options.
246    pub fn run_inline_python_with_options(
247        &mut self,
248        code: &str,
249        options: InlinePythonOptions,
250    ) -> Result<crate::ExecutionOutcome> {
251        let (bundle, entrypoint) = options.build_bundle(code)?;
252        let descriptor = InvocationDescriptor::new(entrypoint);
253        let (session, _) = self
254            .runtime
255            .prepare_session_with_manifest_and_descriptor(bundle, descriptor)?;
256        self.runtime.run_session(&session)
257    }
258}