1use std::collections::{HashMap, VecDeque};
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{Arc, Mutex};
8use std::thread::{self, JoinHandle};
9
10use crossbeam::channel::{unbounded, Receiver, Sender};
11
12use crate::error::{StrykeError, StrykeResult};
13use crate::scope::{AtomicArray, AtomicHash};
14use crate::value::{PerlPpool, StrykeSub, StrykeValue};
15use crate::vm_helper::{Flow, FlowOrError, VMHelper};
16
17pub struct PpoolInner {
19 pub(crate) job_tx: Mutex<Option<Sender<PoolJob>>>,
21 result_rx: Mutex<Receiver<(u64, StrykeValue)>>,
23 pending: Mutex<VecDeque<(u64, StrykeValue)>>,
25 pub(crate) next_order: AtomicU64,
26 collect_from: AtomicU64,
28 workers: Mutex<Option<Vec<JoinHandle<()>>>>,
30}
31
32pub(crate) struct PoolJob {
33 order: u64,
34 sub: Arc<StrykeSub>,
35 arg: StrykeValue,
36 subs: HashMap<String, Arc<StrykeSub>>,
37 capture: Vec<(String, StrykeValue)>,
38 atomic_arrays: Vec<(String, AtomicArray)>,
39 atomic_hashes: Vec<(String, AtomicHash)>,
40}
41
42impl PerlPpool {
43 pub(crate) fn submit(
44 &self,
45 interp: &mut VMHelper,
46 args: &[StrykeValue],
47 line: usize,
48 ) -> StrykeResult<StrykeValue> {
49 if args.is_empty() {
50 return Err(StrykeError::runtime(
51 "submit() expects a code reference and optional argument for $_",
52 line,
53 ));
54 }
55 let Some(sub) = args[0].as_code_ref() else {
56 return Err(StrykeError::runtime(
57 "submit() first argument must be a CODE ref",
58 line,
59 ));
60 };
61 let arg = if args.len() >= 2 {
64 args[1].clone()
65 } else {
66 interp.scope.get_scalar("_").clone()
67 };
68 let order = self.0.next_order.fetch_add(1, Ordering::SeqCst);
69 let subs = interp.subs.clone();
70 let (capture, atomic_arrays, atomic_hashes) = interp.scope.capture_with_atomics();
71 let job = PoolJob {
72 order,
73 sub: Arc::clone(&sub),
74 arg,
75 subs,
76 capture,
77 atomic_arrays,
78 atomic_hashes,
79 };
80 let tx = self
81 .0
82 .job_tx
83 .lock()
84 .map_err(|_| StrykeError::runtime("ppool: job queue poisoned", line))?;
85 let Some(sender) = tx.as_ref() else {
86 return Err(StrykeError::runtime("ppool: pool shut down", line));
87 };
88 sender
89 .send(job)
90 .map_err(|_| StrykeError::runtime("ppool: submit failed (pool shut down)", line))?;
91 Ok(StrykeValue::UNDEF)
92 }
93
94 pub(crate) fn collect(&self, line: usize) -> StrykeResult<StrykeValue> {
95 let start = self.0.collect_from.load(Ordering::SeqCst);
96 let end = self.0.next_order.load(Ordering::SeqCst);
97 let n = (end - start) as usize;
98 if n == 0 {
99 return Ok(StrykeValue::array(vec![]));
100 }
101
102 let mut slots: Vec<Option<StrykeValue>> = vec![None; n];
103 let mut count = 0usize;
104
105 {
106 let mut pending = self
107 .0
108 .pending
109 .lock()
110 .map_err(|_| StrykeError::runtime("ppool: pending buffer poisoned", line))?;
111 let mut keep = VecDeque::new();
112 for (o, v) in pending.drain(..) {
113 if o >= start && o < end {
114 let idx = (o - start) as usize;
115 if slots[idx].is_none() {
116 slots[idx] = Some(v);
117 count += 1;
118 }
119 } else {
120 keep.push_back((o, v));
121 }
122 }
123 *pending = keep;
124 }
125
126 let rx = self
127 .0
128 .result_rx
129 .lock()
130 .map_err(|_| StrykeError::runtime("ppool: collect lock poisoned", line))?;
131
132 while count < n {
133 let (o, v) = rx.recv().map_err(|_| {
134 StrykeError::runtime("ppool: result channel closed (workers stopped)", line)
135 })?;
136 if o < start {
137 continue;
138 }
139 if o >= end {
140 self.0
141 .pending
142 .lock()
143 .map_err(|_| StrykeError::runtime("ppool: pending buffer poisoned", line))?
144 .push_back((o, v));
145 continue;
146 }
147 let idx = (o - start) as usize;
148 if slots[idx].is_none() {
149 slots[idx] = Some(v);
150 count += 1;
151 }
152 }
153
154 self.0.collect_from.store(end, Ordering::SeqCst);
155 let out: Vec<StrykeValue> = slots
156 .into_iter()
157 .map(|s| s.unwrap_or(StrykeValue::UNDEF))
158 .collect();
159 Ok(StrykeValue::array(out))
160 }
161}
162
163impl Drop for PpoolInner {
164 fn drop(&mut self) {
165 if let Ok(mut g) = self.job_tx.lock() {
166 let _ = g.take();
167 }
168 if let Ok(mut g) = self.workers.lock() {
169 if let Some(handles) = g.take() {
170 for h in handles {
171 let _ = h.join();
172 }
173 }
174 }
175 }
176}
177
178fn worker_loop(job_rx: Receiver<PoolJob>, result_tx: Sender<(u64, StrykeValue)>) {
179 while let Ok(job) = job_rx.recv() {
180 let mut interp = VMHelper::new();
181 interp.subs = job.subs;
182 interp.scope.restore_capture(&job.capture);
183 interp
184 .scope
185 .restore_atomics(&job.atomic_arrays, &job.atomic_hashes);
186 if let Some(env) = job.sub.closure_env.as_ref() {
187 interp.scope.restore_capture(env);
188 }
189 interp.enable_parallel_guard();
190 interp.scope.set_topic(job.arg);
191 interp.scope_push_hook();
192 let val = match interp.exec_block_no_scope(&job.sub.body) {
193 Ok(v) => v,
194 Err(FlowOrError::Flow(Flow::Return(v))) => v,
195 Err(_) => StrykeValue::UNDEF,
196 };
197 interp.scope_pop_hook();
198 let _ = result_tx.send((job.order, val));
199 }
200}
201
202pub fn create_pool(workers: usize) -> StrykeResult<StrykeValue> {
205 let workers = workers.clamp(1, 256);
206 let (job_tx, job_rx): (Sender<PoolJob>, Receiver<PoolJob>) = unbounded();
207 type ResultMsg = (u64, StrykeValue);
208 let (result_tx, result_rx): (Sender<ResultMsg>, Receiver<ResultMsg>) = unbounded();
209
210 let mut handles = Vec::with_capacity(workers);
211 for _ in 0..workers {
212 let jrx = job_rx.clone();
213 let rtx = result_tx.clone();
214 handles.push(thread::spawn(move || worker_loop(jrx, rtx)));
215 }
216 drop(job_rx);
217 drop(result_tx);
218
219 let inner = Arc::new(PpoolInner {
220 job_tx: Mutex::new(Some(job_tx)),
221 result_rx: Mutex::new(result_rx),
222 pending: Mutex::new(VecDeque::new()),
223 next_order: AtomicU64::new(0),
224 collect_from: AtomicU64::new(0),
225 workers: Mutex::new(Some(handles)),
226 });
227
228 Ok(StrykeValue::ppool(PerlPpool(inner)))
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::parse;
235
236 #[test]
237 fn test_ppool_basic() {
238 let mut interp = VMHelper::new();
239 let pool_val = create_pool(2).expect("create_pool");
240 let pool = pool_val.as_ppool().expect("as_ppool");
241
242 let prog = parse("{ $_ * 2 }").expect("parse");
243 let body = match &prog.statements[0].kind {
244 crate::ast::StmtKind::Block(b) => b.clone(),
245 _ => panic!("expected block"),
246 };
247
248 let sub_val = StrykeValue::code_ref(Arc::new(StrykeSub {
249 name: "anon".to_string(),
250 params: vec![],
251 body,
252 prototype: None,
253 closure_env: None,
254 fib_like: None,
255 }));
256
257 for i in 1..=5 {
258 pool.submit(&mut interp, &[sub_val.clone(), StrykeValue::integer(i)], 1)
259 .expect("submit");
260 }
261
262 let results = pool.collect(1).expect("collect");
263 let arr = results.as_array_vec().expect("array");
264 assert_eq!(arr.len(), 5);
265 let mut ints: Vec<i64> = arr.iter().map(|v| v.to_int()).collect();
266 ints.sort();
267 assert_eq!(ints, vec![2, 4, 6, 8, 10]);
268 }
269}