1use crate::{invoke, CancelOnDrop, CancellationToken, InvocationControl, ScriptHost, ScriptLimits};
2use mhome_script_api::{ScriptError, ScriptValue};
3use std::{
4 sync::{
5 atomic::{AtomicUsize, Ordering},
6 Arc, Mutex,
7 },
8 thread::JoinHandle,
9};
10use tokio::{
11 sync::{mpsc, oneshot, watch, OwnedSemaphorePermit, Semaphore},
12 task::{JoinSet, LocalSet},
13};
14
15#[derive(Clone, Copy, Debug)]
17pub enum Admission {
18 Request,
19 Hook,
20}
21
22#[derive(Clone, Copy, Debug)]
23pub struct WorkerConfig {
24 pub threads: usize,
25 pub requests: usize,
26 pub hooks: usize,
27}
28
29impl Default for WorkerConfig {
30 fn default() -> Self {
31 Self {
32 threads: 2,
33 requests: 4,
34 hooks: 2,
35 }
36 }
37}
38
39struct Job {
40 source: Arc<str>,
41 args: Vec<ScriptValue>,
42 host: Arc<dyn ScriptHost>,
43 control: InvocationControl,
44 limits: ScriptLimits,
45 reply: oneshot::Sender<Result<ScriptValue, ScriptError>>,
46 _permit: OwnedSemaphorePermit,
47}
48
49struct Worker {
50 queue: mpsc::Sender<Job>,
51 finished: watch::Receiver<bool>,
52 thread: Mutex<Option<JoinHandle<()>>>,
53}
54
55struct Inner {
56 workers: Vec<Worker>,
57 requests: Arc<Semaphore>,
58 hooks: Arc<Semaphore>,
59 next: AtomicUsize,
60 shutdown: CancellationToken,
61}
62
63impl Drop for Inner {
64 fn drop(&mut self) {
65 self.shutdown.cancel();
66 }
67}
68
69#[derive(Clone)]
74pub struct ScriptWorkers(Arc<Inner>);
75
76impl ScriptWorkers {
77 pub fn new(config: WorkerConfig) -> Result<Self, ScriptError> {
79 if !(1..=64).contains(&config.threads) || config.requests == 0 || config.hooks == 0 {
80 return Err(ScriptError::new(
81 "INVALID_CONFIG",
82 "Script worker counts must be positive and threads at most 64",
83 ));
84 }
85 let capacity = config
86 .requests
87 .checked_add(config.hooks)
88 .filter(|n| *n <= Semaphore::MAX_PERMITS)
89 .ok_or_else(|| ScriptError::new("INVALID_CONFIG", "Script worker capacity overflow"))?;
90 let mut inner = Inner {
91 workers: Vec::new(),
92 requests: Arc::new(Semaphore::new(config.requests)),
93 hooks: Arc::new(Semaphore::new(config.hooks)),
94 next: AtomicUsize::new(0),
95 shutdown: CancellationToken::new(),
96 };
97 for index in 0..config.threads {
98 let runtime = tokio::runtime::Builder::new_current_thread()
99 .enable_all()
100 .build()
101 .map_err(|e| ScriptError::new("WORKER_START_FAILED", e.to_string()))?;
102 let (queue, receiver) = mpsc::channel(capacity);
103 let (finished_tx, finished) = watch::channel(false);
104 let shutdown = inner.shutdown.clone();
105 let thread = std::thread::Builder::new()
106 .name(format!("script-{index}"))
107 .spawn(move || {
108 let _finished = Finished(finished_tx);
109 let worker_runtime = runtime;
113 worker_runtime.block_on(LocalSet::new().run_until(worker(receiver, shutdown)));
114 })
115 .map_err(|e| ScriptError::new("WORKER_START_FAILED", e.to_string()))?;
116 inner.workers.push(Worker {
117 queue,
118 finished,
119 thread: Mutex::new(Some(thread)),
120 });
121 }
122 Ok(Self(Arc::new(inner)))
123 }
124
125 pub async fn invoke(
126 &self,
127 source: Arc<str>,
128 args: Vec<ScriptValue>,
129 host: Arc<dyn ScriptHost>,
130 mut control: InvocationControl,
131 limits: ScriptLimits,
132 admission: Admission,
133 ) -> Result<ScriptValue, ScriptError> {
134 control.shutdown = self.0.shutdown.clone();
135 control.check()?;
136 let permits = match admission {
137 Admission::Request => &self.0.requests,
138 Admission::Hook => &self.0.hooks,
139 };
140 let permit = permits.clone().try_acquire_owned().map_err(|_| {
141 ScriptError::new("SCRIPT_BUSY", "Script execution capacity is exhausted")
142 })?;
143 control.cancel = control.cancel.child_token();
144 let _cancel_on_drop = CancelOnDrop(control.cancel.clone());
145 let (reply, result) = oneshot::channel();
146 let job = Job {
147 source,
148 args,
149 host,
150 control: control.clone(),
151 limits,
152 reply,
153 _permit: permit,
154 };
155 let index = self.0.next.fetch_add(1, Ordering::Relaxed) % self.0.workers.len();
156 self.0.workers[index]
157 .queue
158 .try_send(job)
159 .map_err(|_| ScriptError::new("WORKER_UNAVAILABLE", "Script worker is unavailable"))?;
160 tokio::select! {
161 biased;
162 _ = control.cancelled() => Err(ScriptError::new("CANCELLED", "Script invocation cancelled")),
163 _ = tokio::time::sleep_until(control.deadline.into()) => Err(ScriptError::new("DEADLINE_EXCEEDED", "Script invocation timed out")),
164 result = result => result.unwrap_or_else(|_| Err(ScriptError::new("WORKER_FAILED", "Script worker failed"))),
165 }
166 }
167
168 pub async fn shutdown(&self) {
171 self.0.shutdown.cancel();
172 for worker in &self.0.workers {
173 let mut finished = worker.finished.clone();
174 let _ = finished.wait_for(|done| *done).await;
175 if let Some(thread) = worker
177 .thread
178 .lock()
179 .unwrap_or_else(|e| e.into_inner())
180 .take()
181 {
182 let _ = thread.join();
183 }
184 }
185 }
186}
187
188struct Finished(watch::Sender<bool>);
189impl Drop for Finished {
190 fn drop(&mut self) {
191 self.0.send_replace(true);
192 }
193}
194
195async fn worker(mut queue: mpsc::Receiver<Job>, shutdown: CancellationToken) {
196 let mut running = JoinSet::new();
197 loop {
198 tokio::select! {
199 biased;
200 _ = shutdown.cancelled() => break,
201 job = queue.recv() => match job {
202 Some(job) => {
203 running.spawn_local(async move {
204 let result = invoke(&job.source, job.args, job.host, job.control, job.limits).await;
205 let _ = job.reply.send(result);
206 drop(job._permit);
208 });
209 }
210 None => break,
211 },
212 _ = running.join_next(), if !running.is_empty() => {},
213 }
214 }
215 queue.close();
216 while queue.try_recv().is_ok() {}
217 running.abort_all();
218 while running.join_next().await.is_some() {}
219}