1use std::path::PathBuf;
4use std::sync::Arc;
5use std::time::Duration;
6
7use async_trait::async_trait;
8use monty_pool::{
9 Checkout, Pool, PoolConfig, PoolError, ReplConfig, ResumeValue, TurnEvent, on_print_sync,
10};
11use monty_types::{ExcType, MontyException, MontyObject, NameLookupResult, ResourceLimits};
12use serde_json::{Map, Value};
13
14use super::CodeLimits;
15use super::monty::{from_json, key, to_json};
16use super::sandbox::{CodeSandbox, Execution, Suspension};
17
18#[derive(Clone)]
26pub struct MontyPool {
27 pool: Arc<Pool>,
28}
29
30impl std::fmt::Debug for MontyPool {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 f.debug_struct("MontyPool").finish_non_exhaustive()
33 }
34}
35
36impl MontyPool {
37 pub async fn spawn(binary: impl Into<PathBuf>, workers: usize) -> Result<Self, String> {
41 let mut config = PoolConfig::subprocess(binary);
42 config.min_processes = 1.min(workers);
43 config.max_processes = workers.max(1);
44 config.request_timeout = Some(Duration::from_secs(30));
45 config.checkout_timeout = Some(Duration::from_secs(5));
46 Self::with_config(config).await
47 }
48
49 pub async fn with_config(config: PoolConfig) -> Result<Self, String> {
51 let pool = Pool::new(config).await.map_err(|e| e.to_string())?;
52 Ok(Self {
53 pool: Arc::new(pool),
54 })
55 }
56}
57
58#[async_trait]
59impl CodeSandbox for MontyPool {
60 async fn start(
61 &self,
62 program: &str,
63 functions: &[String],
64 limits: &CodeLimits,
65 ) -> (Box<dyn Execution>, Suspension) {
66 let repl = ReplConfig {
67 limits: Some(ResourceLimits {
68 max_feed_duration: Some(limits.cpu),
69 max_memory: Some(limits.memory),
70 ..ResourceLimits::default()
71 }),
72 ..ReplConfig::default()
73 };
74 let mut execution = Worker {
75 checkout: None,
76 functions: functions.to_vec(),
77 printed: String::new(),
78 cap: limits.output_chars,
79 };
80 let mut checkout = match self.pool.checkout(&repl).await {
81 Ok(checkout) => checkout,
82 Err(e) => {
83 return (
84 Box::new(execution),
85 Suspension::failed(format!("no sandbox worker: {e}")),
86 );
87 }
88 };
89 let inputs: Vec<(String, MontyObject)> = functions
90 .iter()
91 .map(|name| (name.clone(), MontyObject::function(name.clone(), None)))
92 .collect();
93 let event = {
94 let printed = &mut execution.printed;
95 let cap = execution.cap;
96 let mut on_print = on_print_sync(move |_, text| append(printed, text, cap));
97 checkout
98 .feed(program, inputs, Vec::new(), true, &mut on_print)
99 .await
100 };
101 execution.checkout = Some(checkout);
102 let first = execution.settle(event).await;
103 (Box::new(execution), first)
104 }
105}
106
107struct Worker {
109 checkout: Option<Checkout>,
112 functions: Vec<String>,
113 printed: String,
114 cap: usize,
115}
116
117enum Next {
119 Resume(ResumeValue),
120 Undefined,
121}
122
123fn append(printed: &mut String, text: &str, cap: usize) {
124 if printed.chars().count() < cap {
125 printed.push_str(text);
126 }
127}
128
129impl Worker {
130 async fn settle(&mut self, mut event: Result<TurnEvent, PoolError>) -> Suspension {
133 loop {
134 let next = match event {
135 Ok(TurnEvent::FunctionCall {
136 function_name,
137 args,
138 ..
139 }) => {
140 if self.functions.contains(&function_name) {
141 let positional = args.args().map(to_json).collect();
142 let keywords: Map<String, Value> =
143 args.kwargs().map(|(k, v)| (key(k), to_json(v))).collect();
144 return Suspension::Call {
145 function: function_name,
146 positional,
147 keywords,
148 };
149 }
150 Next::Resume(ResumeValue::Error(MontyException::new(
151 ExcType::NameError,
152 Some(format!("name '{function_name}' is not defined")),
153 )))
154 }
155 Ok(TurnEvent::OsCall { .. }) => {
156 Next::Resume(ResumeValue::Error(MontyException::new(
157 ExcType::PermissionError,
158 Some("this sandbox has no file system, network or clock access".into()),
159 )))
160 }
161 Ok(TurnEvent::NameLookup { .. }) => Next::Undefined,
162 Ok(TurnEvent::ResolveFutures { .. }) => {
163 self.checkout = None;
165 return self.finished(Err(
166 "async code is not supported; call the functions directly".into(),
167 ));
168 }
169 Ok(TurnEvent::Complete(value)) => {
170 let result = to_json(value.as_ref());
171 self.release().await;
172 return self.finished(Ok(result));
173 }
174 Err(PoolError::Runtime(exception)) => {
175 if matches!(
178 exception.exc_type(),
179 ExcType::TimeoutError | ExcType::MemoryError
180 ) {
181 self.checkout = None;
182 } else {
183 self.release().await;
184 }
185 return self.finished(Err(exception.to_string()));
186 }
187 Err(error) => {
188 self.checkout = None;
189 return self.finished(Err(format!("the sandbox worker failed: {error}")));
190 }
191 };
192 let Some(checkout) = self.checkout.as_mut() else {
193 return Suspension::failed("the program has ended");
194 };
195 let printed = &mut self.printed;
196 let cap = self.cap;
197 let mut on_print = on_print_sync(move |_, text| append(printed, text, cap));
198 event = match next {
199 Next::Resume(value) => checkout.resume(value, &mut on_print).await,
200 Next::Undefined => {
201 checkout
202 .resume_name_lookup(NameLookupResult::Undefined, &mut on_print)
203 .await
204 }
205 };
206 }
207 }
208
209 async fn release(&mut self) {
211 if let Some(checkout) = self.checkout.take() {
212 let _ = checkout.finish().await;
213 }
214 }
215
216 fn finished(&mut self, result: Result<Value, String>) -> Suspension {
217 Suspension::Finished {
218 result,
219 printed: std::mem::take(&mut self.printed),
220 }
221 }
222}
223
224#[async_trait]
225impl Execution for Worker {
226 async fn resume(&mut self, answer: Result<Value, String>) -> Suspension {
227 let Some(checkout) = self.checkout.as_mut() else {
228 return Suspension::failed("the program has ended");
229 };
230 let value = match answer {
231 Ok(value) => ResumeValue::Return(from_json(&value)),
232 Err(reason) => {
233 ResumeValue::Error(MontyException::new(ExcType::RuntimeError, Some(reason)))
234 }
235 };
236 let event = {
237 let printed = &mut self.printed;
238 let cap = self.cap;
239 let mut on_print = on_print_sync(move |_, text| append(printed, text, cap));
240 checkout.resume(value, &mut on_print).await
241 };
242 self.settle(event).await
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 fn binary() -> Option<PathBuf> {
253 std::env::var_os("MONTY_BIN").map(PathBuf::from)
254 }
255
256 #[tokio::test]
257 async fn a_program_runs_in_a_worker_and_calls_back() {
258 let Some(bin) = binary() else { return };
259 let pool = MontyPool::spawn(bin, 2).await.expect("pool");
260 let functions = vec!["double".to_string()];
261 let (mut run, stop) = pool
262 .start(
263 "print('hi')\n[double(21), sum(range(4))]",
264 &functions,
265 &CodeLimits::default(),
266 )
267 .await;
268 assert!(matches!(stop, Suspension::Call { .. }), "{stop:?}");
269 let stop = run.resume(Ok(serde_json::json!(42))).await;
270 assert_eq!(
271 stop,
272 Suspension::Finished {
273 result: Ok(serde_json::json!([42, 6])),
274 printed: "hi\n".into(),
275 }
276 );
277 }
278
279 #[tokio::test]
280 async fn a_huge_allocation_kills_only_the_worker() {
281 let Some(bin) = binary() else { return };
282 let pool = MontyPool::spawn(bin, 1).await.expect("pool");
283 let (_, stop) = pool
284 .start("len('a' * 10**10)", &[], &CodeLimits::default())
285 .await;
286 let Suspension::Finished { result: Err(e), .. } = stop else {
287 panic!("expected a memory error, got {stop:?}");
288 };
289 assert!(e.contains("MemoryError") || e.contains("memory"), "{e}");
290 let (_, stop) = pool.start("1 + 1", &[], &CodeLimits::default()).await;
292 assert_eq!(
293 stop,
294 Suspension::Finished {
295 result: Ok(serde_json::json!(2)),
296 printed: String::new(),
297 }
298 );
299 }
300}