Skip to content

Commit 6909c27

Browse files
committed
refactor: max executions
1 parent f21671e commit 6909c27

6 files changed

Lines changed: 106 additions & 35 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Main entry point. Manages a pool of JS workers, each with loaded modules.
5656
- `Pool::builder()` — creates a new pool builder
5757
- `PoolBuilder::max_size(n)` — sets worker pool size
5858
- `PoolBuilder::with_evaluation_timeout(duration)` — module load timeout
59+
- `PoolBuilder::with_max_executions(limit)` — recycle workers after n executions (prevents memory leaks)
5960
- `PoolBuilder::add_module(name, code)` — loads a module into all workers
6061
- `exec(module_name, function_name, params, ExecOptions)` — executes a function
6162

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ mod runtime;
1010
mod worker;
1111

1212
pub use manager::{ModuleDefinition, WorkerManager};
13-
pub use options::{DomainPermission, ExecOptions, RecyclePolicy, WorkerOptions};
13+
pub use options::{DomainPermission, ExecOptions, MaxExecutions, RecyclePolicy, WorkerOptions};
1414
pub use orama_extension::{OutputChannel, SharedCache};
1515
pub use parameters::{FunctionParameters, TryIntoFunctionParameters};
1616
pub use pool::{Pool, PoolBuilder};

src/manager.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,9 @@ impl Manager for WorkerManager {
9797
.with_cache(cache)
9898
.with_domain_permission(worker_options.domain_permission)
9999
.with_evaluation_timeout(worker_options.evaluation_timeout)
100+
.with_max_executions(worker_options.max_executions)
100101
.with_version(version);
101102

102-
if let Some(max_executions) = worker_options.max_executions {
103-
builder = builder.with_max_executions(max_executions);
104-
}
105-
106103
for (name, def) in modules {
107104
builder = builder.add_module(name, def.code);
108105
}

src/options.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,33 @@ use crate::OutputChannel;
55

66
pub use crate::permission::DomainPermission;
77

8+
/// Controls how many times a worker can execute functions before being recycled.
9+
///
10+
/// Worker recycling helps prevent memory leaks from accumulated module cache and state.
11+
/// When a worker reaches its execution limit, it will be invalidated and recreated
12+
/// with a fresh runtime on the next use.
13+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14+
pub enum MaxExecutions {
15+
Unlimited,
16+
Limited(u64),
17+
}
18+
19+
impl Default for MaxExecutions {
20+
fn default() -> Self {
21+
MaxExecutions::Limited(100)
22+
}
23+
}
24+
25+
impl MaxExecutions {
26+
/// Check if the execution count has been exceeded
27+
pub fn is_exceeded(&self, count: u64) -> bool {
28+
match self {
29+
MaxExecutions::Unlimited => false,
30+
MaxExecutions::Limited(max) => count >= *max,
31+
}
32+
}
33+
}
34+
835
/// Policy for when to recycle/invalidate a runtime
936
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1037
pub enum RecyclePolicy {
@@ -65,7 +92,7 @@ pub struct WorkerOptions {
6592
pub evaluation_timeout: Duration,
6693
pub domain_permission: DomainPermission,
6794
pub recycle_policy: RecyclePolicy,
68-
pub max_executions: Option<u64>,
95+
pub max_executions: MaxExecutions,
6996
}
7097

7198
impl WorkerOptions {
@@ -88,8 +115,8 @@ impl WorkerOptions {
88115
self
89116
}
90117

91-
pub fn with_max_executions(mut self, max: u64) -> Self {
92-
self.max_executions = Some(max);
118+
pub fn with_max_executions(mut self, max: MaxExecutions) -> Self {
119+
self.max_executions = max;
93120
self
94121
}
95122
}
@@ -100,7 +127,7 @@ impl Default for WorkerOptions {
100127
evaluation_timeout: Duration::from_secs(5),
101128
domain_permission: DomainPermission::DenyAll,
102129
recycle_policy: RecyclePolicy::default(),
103-
max_executions: None,
130+
max_executions: MaxExecutions::default(),
104131
}
105132
}
106133
}

src/pool.rs

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::{orama_extension::SharedCache, RecyclePolicy};
99

1010
use super::{
1111
manager::{ModuleDefinition, WorkerManager},
12-
options::{DomainPermission, ExecOptions, WorkerOptions},
12+
options::{DomainPermission, ExecOptions, MaxExecutions, WorkerOptions},
1313
parameters::TryIntoFunctionParameters,
1414
runtime::RuntimeError,
1515
worker::WorkerBuilder,
@@ -138,7 +138,7 @@ pub struct PoolBuilder {
138138
domain_permission: Option<DomainPermission>,
139139
evaluation_timeout: Option<std::time::Duration>,
140140
recycle_policy: Option<RecyclePolicy>,
141-
max_executions: Option<u64>,
141+
max_executions: Option<MaxExecutions>,
142142
}
143143

144144
impl PoolBuilder {
@@ -150,7 +150,7 @@ impl PoolBuilder {
150150
domain_permission: None,
151151
evaluation_timeout: None,
152152
recycle_policy: None,
153-
max_executions: Some(100),
153+
max_executions: None,
154154
}
155155
}
156156

@@ -178,9 +178,8 @@ impl PoolBuilder {
178178
self
179179
}
180180

181-
/// Set the maximum number of executions before recycling the runtime
182-
/// to prevent memory leaks from accumulated module cache.
183-
pub fn with_max_executions(mut self, max: u64) -> Self {
181+
/// Set the maximum number of executions before recycling workers in the pool.
182+
pub fn with_max_executions(mut self, max: MaxExecutions) -> Self {
184183
self.max_executions = Some(max);
185184
self
186185
}
@@ -209,7 +208,7 @@ impl PoolBuilder {
209208
evaluation_timeout: self.evaluation_timeout.unwrap_or(Duration::from_secs(5)),
210209
domain_permission: self.domain_permission.unwrap_or_default(),
211210
recycle_policy: self.recycle_policy.unwrap_or_default(),
212-
max_executions: self.max_executions,
211+
max_executions: self.max_executions.unwrap_or_default(),
213212
};
214213

215214
// Validate that all modules can be loaded within the evaluation timeout
@@ -1468,4 +1467,57 @@ mod tests {
14681467
RuntimeError::MissingModule(name) if name == "nonexistent"
14691468
));
14701469
}
1470+
1471+
#[tokio::test]
1472+
async fn test_pool_max_executions() {
1473+
let _ = tracing_subscriber::fmt::try_init();
1474+
1475+
let counter_code = r#"
1476+
let callCount = 0;
1477+
function increment() {
1478+
callCount++;
1479+
return callCount;
1480+
}
1481+
export default { increment };
1482+
"#;
1483+
1484+
let pool = Pool::builder()
1485+
.max_size(1)
1486+
.add_module("counter", counter_code.to_string())
1487+
.with_max_executions(MaxExecutions::Limited(2))
1488+
.build()
1489+
.await
1490+
.unwrap();
1491+
1492+
// First 3 executions should increment the counter
1493+
let result1: i32 = pool
1494+
.exec("counter", "increment", (), ExecOptions::new())
1495+
.await
1496+
.unwrap();
1497+
assert_eq!(result1, 1);
1498+
1499+
let result2: i32 = pool
1500+
.exec("counter", "increment", (), ExecOptions::new())
1501+
.await
1502+
.unwrap();
1503+
assert_eq!(result2, 2);
1504+
1505+
// After 2 executions, the worker should be recycled
1506+
// and the counter should reset to 1
1507+
let result4: i32 = pool
1508+
.exec("counter", "increment", (), ExecOptions::new())
1509+
.await
1510+
.unwrap();
1511+
assert_eq!(
1512+
result4, 1,
1513+
"Counter should reset after max_executions is reached"
1514+
);
1515+
1516+
// Verify it continues to work correctly after recycling
1517+
let result5: i32 = pool
1518+
.exec("counter", "increment", (), ExecOptions::new())
1519+
.await
1520+
.unwrap();
1521+
assert_eq!(result5, 2);
1522+
}
14711523
}

src/worker.rs

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::orama_extension::SharedCache;
88
use crate::runtime::ModuleName;
99

1010
use super::{
11-
options::{DomainPermission, ExecOptions},
11+
options::{DomainPermission, ExecOptions, MaxExecutions},
1212
parameters::TryIntoFunctionParameters,
1313
runtime::{Runtime, RuntimeError},
1414
};
@@ -27,7 +27,7 @@ pub struct Worker {
2727
cache: SharedCache,
2828
domain_permission: DomainPermission,
2929
evaluation_timeout: std::time::Duration,
30-
max_executions: Option<u64>,
30+
max_executions: MaxExecutions,
3131
execution_count: u64,
3232
version: u64,
3333
}
@@ -38,7 +38,7 @@ impl Worker {
3838
cache: SharedCache,
3939
domain_permission: DomainPermission,
4040
evaluation_timeout: std::time::Duration,
41-
max_executions: Option<u64>,
41+
max_executions: MaxExecutions,
4242
version: u64,
4343
) -> Self {
4444
Self {
@@ -121,15 +121,10 @@ impl Worker {
121121
}
122122

123123
// Check if we need to invalidate the runtime due to execution limit
124-
if let Some(max_executions) = self.max_executions {
125-
if self.execution_count >= max_executions {
126-
warn!(
127-
"Worker reached max executions limit ({}), invalidating runtime",
128-
max_executions
129-
);
130-
self.runtime = None;
131-
self.execution_count = 0;
132-
}
124+
if self.max_executions.is_exceeded(self.execution_count) {
125+
warn!("Worker reached max executions limit, invalidating runtime");
126+
self.runtime = None;
127+
self.execution_count = 0;
133128
}
134129

135130
let domain_permission = exec_options
@@ -208,7 +203,7 @@ pub struct WorkerBuilder {
208203
cache: Option<SharedCache>,
209204
domain_permission: Option<DomainPermission>,
210205
evaluation_timeout: Option<std::time::Duration>,
211-
max_executions: Option<u64>,
206+
max_executions: MaxExecutions,
212207
version: u64,
213208
}
214209

@@ -220,7 +215,7 @@ impl WorkerBuilder {
220215
cache: None,
221216
domain_permission: None,
222217
evaluation_timeout: None,
223-
max_executions: None,
218+
max_executions: MaxExecutions::default(),
224219
version: 0,
225220
}
226221
}
@@ -254,10 +249,9 @@ impl WorkerBuilder {
254249
self
255250
}
256251

257-
/// Set the maximum number of executions before recycling the runtime
258-
/// to prevent memory leaks from accumulated module cache.
259-
pub fn with_max_executions(mut self, max: u64) -> Self {
260-
self.max_executions = Some(max);
252+
/// Set the maximum number of executions before recycling the runtime.
253+
pub fn with_max_executions(mut self, max: MaxExecutions) -> Self {
254+
self.max_executions = max;
261255
self
262256
}
263257

@@ -440,7 +434,7 @@ mod tests {
440434

441435
let mut worker = Worker::builder()
442436
.add_module("counter", counter_code.to_string())
443-
.with_max_executions(3)
437+
.with_max_executions(MaxExecutions::Limited(3))
444438
.build()
445439
.await
446440
.unwrap();

0 commit comments

Comments
 (0)