Skip to content

Commit 1381b2a

Browse files
committed
fix: timeout
1 parent 9025e2a commit 1381b2a

4 files changed

Lines changed: 72 additions & 51 deletions

File tree

src/manager.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use super::{
1818
};
1919

2020
/// Definition of a module to be loaded into workers
21-
#[derive(Clone)]
21+
#[derive(Clone, Debug)]
2222
pub struct ModuleDefinition {
2323
pub code: Arc<str>,
2424
}
@@ -29,7 +29,7 @@ pub struct WorkerManager {
2929
modules: Arc<RwLock<HashMap<String, ModuleDefinition>>>,
3030
cache: SharedCache,
3131
version: Arc<AtomicU64>,
32-
worker_options: WorkerOptions,
32+
pub(crate) worker_options: WorkerOptions,
3333
}
3434

3535
impl WorkerManager {

src/options.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ pub use crate::permission::DomainPermission;
88
/// Policy for when to recycle/invalidate a runtime
99
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1010
pub enum RecyclePolicy {
11+
#[default]
12+
Never,
1113
/// Recycle on timeout only (current default behavior)
1214
OnTimeout,
1315
/// Recycle on any error
1416
OnError,
1517
/// Recycle on timeout or error
16-
#[default]
1718
OnTimeoutOrError,
1819
}
1920

src/pool.rs

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use std::collections::HashMap;
22
use std::time::Duration;
33

4+
use deadpool::managed::Object;
45
use deno_core::ModuleCodeString;
56
use serde::de::DeserializeOwned;
67

7-
use crate::orama_extension::SharedCache;
8+
use crate::{orama_extension::SharedCache, RecyclePolicy};
89

910
use super::{
1011
manager::{ModuleDefinition, WorkerManager},
@@ -40,18 +41,34 @@ impl Pool {
4041
Input: TryIntoFunctionParameters + Send + 'static,
4142
Output: DeserializeOwned + Send + 'static,
4243
{
43-
let mut worker = self.inner.get().await.map_err(|e| match e {
44-
deadpool::managed::PoolError::Backend(err) => match err {
45-
// The recycle policy of the poll will prevent this
46-
RuntimeError::Dead => unreachable!(),
47-
_ => RuntimeError::Unknown(err.to_string()),
48-
},
49-
_ => RuntimeError::Unknown(e.to_string()),
50-
})?;
51-
52-
worker
44+
let mut worker = self.get_worker().await?;
45+
let result = worker
5346
.exec(module_name, function_name, params, exec_options)
54-
.await
47+
.await;
48+
49+
if let Err(ref err) = result {
50+
let recycle_policy = self.manager.worker_options.recycle_policy;
51+
52+
let should_discard = match recycle_policy {
53+
RecyclePolicy::Never => false,
54+
RecyclePolicy::OnTimeout => {
55+
matches!(err, RuntimeError::ExecTimeout)
56+
}
57+
RecyclePolicy::OnError => !matches!(err, RuntimeError::NetworkPermissionDenied(_)),
58+
RecyclePolicy::OnTimeoutOrError => {
59+
matches!(err, RuntimeError::ExecTimeout)
60+
|| !matches!(err, RuntimeError::NetworkPermissionDenied(_))
61+
}
62+
};
63+
64+
if should_discard {
65+
// Take the worker out so it won't be returned to the pool
66+
// This will cause the worker to be dropped and a new one created on next use
67+
let _ = deadpool::managed::Object::take(worker);
68+
}
69+
}
70+
71+
result
5572
}
5673

5774
/// Add or update a module in the pool
@@ -62,20 +79,39 @@ impl Pool {
6279
) -> Result<(), RuntimeError> {
6380
let name = name.into();
6481
let code: ModuleCodeString = code.into();
82+
let (runtime_code, module_code) = code.into_cheap_copy();
83+
84+
let worker = self.get_worker().await?;
85+
// Take the worker out so it won't be returned to the pool
86+
// This will cause the worker to be dropped and a new one created on next use
87+
// So we do not create inconsistent workers.
88+
let mut worker = deadpool::managed::Object::take(worker);
89+
worker.add_module(name.clone(), runtime_code).await?;
6590

6691
let mut modules = self.manager.modules();
6792

6893
modules.insert(
6994
name.clone(),
7095
ModuleDefinition {
71-
code: code.as_str().into(),
96+
code: module_code.as_str().into(),
7297
},
7398
);
7499

75100
self.manager.update_modules(modules);
76101

77102
Ok(())
78103
}
104+
105+
async fn get_worker(&self) -> Result<Object<WorkerManager>, RuntimeError> {
106+
self.inner.get().await.map_err(|e| match e {
107+
deadpool::managed::PoolError::Backend(err) => match err {
108+
// The recycle policy of the poll will prevent this
109+
RuntimeError::Dead => unreachable!(),
110+
_ => err,
111+
},
112+
_ => RuntimeError::Unknown(e.to_string()),
113+
})
114+
}
79115
}
80116

81117
/// Builder for creating a Pool.
@@ -84,7 +120,7 @@ pub struct PoolBuilder {
84120
max_size: usize,
85121
domain_permission: Option<DomainPermission>,
86122
evaluation_timeout: Option<std::time::Duration>,
87-
recycle_policy: Option<super::options::RecyclePolicy>,
123+
recycle_policy: Option<RecyclePolicy>,
88124
}
89125

90126
impl PoolBuilder {
@@ -118,7 +154,7 @@ impl PoolBuilder {
118154
}
119155

120156
/// Set the recycle policy for all workers
121-
pub fn with_recycle_policy(mut self, policy: super::options::RecyclePolicy) -> Self {
157+
pub fn with_recycle_policy(mut self, policy: RecyclePolicy) -> Self {
122158
self.recycle_policy = Some(policy);
123159
self
124160
}
@@ -1117,24 +1153,19 @@ mod tests {
11171153
let pool = Pool::builder()
11181154
.max_size(1)
11191155
.with_evaluation_timeout(Duration::from_millis(100))
1120-
.add_module("expensive", expensive_code.to_string())
11211156
.build()
11221157
.unwrap();
11231158

1124-
let result: Result<i32, RuntimeError> = pool
1125-
.exec("expensive", "getValue", (), ExecOptions::new())
1159+
let result = pool
1160+
.add_module("expensive", expensive_code.to_string())
11261161
.await;
11271162

11281163
assert!(result.is_err(), "Module evaluation should timeout");
11291164
assert!(matches!(result.unwrap_err(), RuntimeError::InitTimeout));
11301165

11311166
// also on adding a module
1132-
pool.add_module("expensive_2", expensive_code.to_string())
1133-
.await
1134-
.unwrap();
1135-
1136-
let result: Result<i32, RuntimeError> = pool
1137-
.exec("expensive_2", "getValue", (), ExecOptions::new())
1167+
let result = pool
1168+
.add_module("expensive_2", expensive_code.to_string())
11381169
.await;
11391170

11401171
assert!(result.is_err(), "Module evaluation should timeout");
@@ -1145,14 +1176,11 @@ mod tests {
11451176
function add(a, b) { return a + b; }
11461177
export default { add };
11471178
"#;
1148-
pool.add_module("not_expensive", add_code.to_string())
1149-
.await
1150-
.unwrap();
1151-
1152-
let result: Result<i32, RuntimeError> = pool
1153-
.exec("not_expensive", "add", (1, 2), ExecOptions::new())
1154-
.await;
1155-
1156-
assert!(result.is_ok(), "Should not error");
1179+
let result = pool.add_module("not_expensive", add_code.to_string()).await;
1180+
assert!(
1181+
result.is_ok(),
1182+
"Should not error: {:?}",
1183+
result.unwrap_err()
1184+
);
11571185
}
11581186
}

src/runtime.rs

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,7 @@ pub struct Runtime<Input, Output> {
8787
join_handler: JoinHandle<()>,
8888
sender: tokio::sync::mpsc::Sender<RuntimeEvent>,
8989
exec_count: u64,
90-
timed_out: bool,
91-
errored: bool,
90+
should_recreate: bool,
9291
loaded_modules: HashMap<String, String>, // module_name -> specifier
9392
evaluation_timeout: Duration,
9493
_p: PhantomData<(Input, Output)>,
@@ -108,6 +107,7 @@ impl<Input: TryIntoFunctionParameters + Send, Output: DeserializeOwned + Send +
108107
evaluation_timeout: Duration,
109108
shared_cache: SharedCache,
110109
) -> Result<Self, RuntimeError> {
110+
// Use a larger buffer so events can be queued even if runtime is busy with a long operation
111111
let (sender, mut receiver) = tokio::sync::mpsc::channel::<RuntimeEvent>(1);
112112
let (init_sender1, init_receiver1) =
113113
tokio::sync::oneshot::channel::<Result<IsolateHandle, CoreError>>();
@@ -268,8 +268,7 @@ impl<Input: TryIntoFunctionParameters + Send, Output: DeserializeOwned + Send +
268268
join_handler: thread_id,
269269
sender,
270270
exec_count: 0,
271-
timed_out: false,
272-
errored: false,
271+
should_recreate: false,
273272
loaded_modules: HashMap::new(),
274273
evaluation_timeout,
275274
_p: PhantomData,
@@ -306,11 +305,9 @@ impl<Input: TryIntoFunctionParameters + Send, Output: DeserializeOwned + Send +
306305
.await
307306
.map_err(|_| {
308307
warn!("Module evaluation timeout for {module_name}");
309-
// Needs a forced restart in this case because it will leave the runtime
310-
// in an inconsistent state.
311-
self.timed_out = true;
312-
self.errored = true;
308+
// Terminate to stop expensive module; worker will recreate runtime
313309
self.handler.terminate_execution();
310+
self.should_recreate = true;
314311
RuntimeError::InitTimeout
315312
})?
316313
.unwrap()?;
@@ -409,7 +406,7 @@ impl<Input: TryIntoFunctionParameters + Send, Output: DeserializeOwned + Send +
409406

410407
let output = match output {
411408
Err(_) => {
412-
self.timed_out = true;
409+
self.should_recreate = true;
413410
self.handler.terminate_execution();
414411
return Err(RuntimeError::ExecTimeout);
415412
}
@@ -418,11 +415,6 @@ impl<Input: TryIntoFunctionParameters + Send, Output: DeserializeOwned + Send +
418415
}
419416
Ok(Ok(Ok(t))) => t,
420417
Ok(Ok(Err(e))) => {
421-
// Mark as errored for recycle policy, but NOT for permission errors
422-
// Permission errors are expected behavior (security enforcement), not runtime errors
423-
if !matches!(e, RuntimeError::NetworkPermissionDenied(_)) {
424-
self.errored = true;
425-
}
426418
return Err(e);
427419
}
428420
};
@@ -435,7 +427,7 @@ impl<Input: TryIntoFunctionParameters + Send, Output: DeserializeOwned + Send +
435427

436428
/// Check if the runtime is still alive
437429
pub fn is_alive(&self) -> bool {
438-
!self.join_handler.is_finished()
430+
!self.join_handler.is_finished() && !self.should_recreate
439431
}
440432
}
441433

0 commit comments

Comments
 (0)