Skip to main content

pharos_native/runtime/
job.rs

1use std::sync::Arc;
2#[cfg(not(target_os = "wasi"))]
3use std::sync::Mutex;
4#[cfg(not(target_os = "wasi"))]
5use std::sync::atomic::AtomicU8;
6use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
7#[cfg(not(target_os = "wasi"))]
8use std::thread::{self, JoinHandle};
9#[cfg(not(target_os = "wasi"))]
10use std::time::Duration;
11
12#[cfg(not(target_os = "wasi"))]
13const WORKER_STOPPED: u8 = 0;
14#[cfg(not(target_os = "wasi"))]
15const WORKER_STARTING: u8 = 1;
16#[cfg(not(target_os = "wasi"))]
17const WORKER_RUNNING: u8 = 2;
18#[cfg(not(target_os = "wasi"))]
19const WORKER_STOPPING: u8 = 3;
20
21#[derive(Clone)]
22/// Shared state for one background job worker.
23///
24/// # Fields
25///
26/// - This type stores module-owned state for [`crate::runtime::job::JobWorkerState`].
27///
28/// # Examples
29///
30/// ```rust,ignore
31/// let _ = std::mem::size_of::<pharos_native::runtime::job::JobWorkerState>();
32/// ```
33///
34/// # Related
35///
36/// - [`crate::runtime::job`] for constructors, helpers, and neighboring runtime types.
37pub struct JobWorkerState {
38    running: Arc<AtomicBool>,
39    shutdown: Arc<AtomicBool>,
40    poll_interval_ms: u64,
41    poll_count: Arc<AtomicUsize>,
42    poll_handler: Arc<dyn Fn() + Send + Sync + 'static>,
43    #[cfg(not(target_os = "wasi"))]
44    lifecycle: Arc<AtomicU8>,
45    #[cfg(not(target_os = "wasi"))]
46    thread: Arc<Mutex<Option<JoinHandle<()>>>>,
47}
48
49impl Default for JobWorkerState {
50    /// Returns the default default.
51    fn default() -> Self {
52        Self {
53            running: Arc::new(AtomicBool::new(false)),
54            shutdown: Arc::new(AtomicBool::new(false)),
55            poll_interval_ms: 5_000,
56            poll_count: Arc::new(AtomicUsize::new(0)),
57            poll_handler: Arc::new(|| {}),
58            #[cfg(not(target_os = "wasi"))]
59            lifecycle: Arc::new(AtomicU8::new(WORKER_STOPPED)),
60            #[cfg(not(target_os = "wasi"))]
61            thread: Arc::new(Mutex::new(None)),
62        }
63    }
64}
65
66impl JobWorkerState {
67    /// Configures the interval between completed poll cycles.
68    ///
69    /// # Parameters
70    ///
71    /// - `self`: Existing worker state being reconfigured.
72    /// - `poll_interval_ms`: Milliseconds to wait between completed poll cycles.
73    ///
74    /// # Returns
75    ///
76    /// Returns the updated worker state.
77    ///
78    /// # Examples
79    ///
80    /// ```rust,ignore
81    /// let _result = pharos_native::runtime::job::JobWorkerState::default().with_poll_interval_ms(1000);
82    /// ```
83    ///
84    /// # Related
85    ///
86    /// - [`crate::runtime::job`] for adjacent helpers that share the same runtime surface.
87    pub fn with_poll_interval_ms(mut self, poll_interval_ms: u64) -> Self {
88        self.poll_interval_ms = poll_interval_ms;
89        self
90    }
91
92    /// Connects each poll cycle to the durable runtime job dispatcher.
93    ///
94    /// # Parameters
95    ///
96    /// - `self`: Existing worker state being reconfigured.
97    /// - `poll_handler`: Handler executed once per poll cycle.
98    ///
99    /// # Returns
100    ///
101    /// Returns the updated worker state.
102    ///
103    /// # Examples
104    ///
105    /// ```rust,ignore
106    /// let _result = pharos_native::runtime::job::JobWorkerState::default().with_poll_handler(std::sync::Arc::new(|| {}));
107    /// ```
108    ///
109    /// # Related
110    ///
111    /// - [`crate::runtime::job`] for adjacent helpers that share the same runtime surface.
112    pub fn with_poll_handler(
113        mut self,
114        poll_handler: Arc<dyn Fn() + Send + Sync + 'static>,
115    ) -> Self {
116        self.poll_handler = poll_handler;
117        self
118    }
119
120    /// Requests a bounded shutdown at the next poll or sleep boundary.
121    ///
122    /// # Returns
123    ///
124    /// Returns after the shutdown flag is raised for the worker.
125    ///
126    /// # Examples
127    ///
128    /// ```rust,ignore
129    /// pharos_native::runtime::job::JobWorkerState::default().request_shutdown();
130    /// ```
131    ///
132    /// # Related
133    ///
134    /// - [`crate::runtime::job`] for adjacent helpers that share the same runtime surface.
135    pub fn request_shutdown(&self) {
136        self.shutdown.store(true, Ordering::SeqCst);
137        #[cfg(not(target_os = "wasi"))]
138        {
139            let _ = self.lifecycle.compare_exchange(
140                WORKER_RUNNING,
141                WORKER_STOPPING,
142                Ordering::SeqCst,
143                Ordering::SeqCst,
144            );
145            let _ = self.lifecycle.compare_exchange(
146                WORKER_STARTING,
147                WORKER_STOPPING,
148                Ordering::SeqCst,
149                Ordering::SeqCst,
150            );
151        }
152    }
153
154    /// Returns the number of poll cycles completed by this state.
155    ///
156    /// # Returns
157    ///
158    /// Returns the total completed poll count observed by this worker state.
159    ///
160    /// # Examples
161    ///
162    /// ```rust,ignore
163    /// let _result = pharos_native::runtime::job::JobWorkerState::default().poll_count();
164    /// ```
165    ///
166    /// # Related
167    ///
168    /// - [`crate::runtime::job`] for adjacent helpers that share the same runtime surface.
169    pub fn poll_count(&self) -> usize {
170        self.poll_count.load(Ordering::SeqCst)
171    }
172}
173
174#[cfg(not(target_os = "wasi"))]
175#[derive(Debug, Clone, Copy, Eq, PartialEq)]
176/// Error returned when a managed worker lifecycle operation cannot complete.
177///
178/// # Variants
179///
180/// - `AlreadyRunning`: Supported `JobWorkerLifecycleError` variant.
181/// - `ThreadSpawnFailed`: Supported `JobWorkerLifecycleError` variant.
182/// - `ThreadPanicked`: Supported `JobWorkerLifecycleError` variant.
183/// - `LifecyclePoisoned`: Supported `JobWorkerLifecycleError` variant.
184///
185/// # Examples
186///
187/// ```rust,ignore
188/// let _ = pharos_native::runtime::job::JobWorkerLifecycleError::AlreadyRunning;
189/// ```
190///
191/// # Related
192///
193/// - [`crate::runtime::job`] for constructors, helpers, and neighboring runtime types.
194pub enum JobWorkerLifecycleError {
195    /// A worker is already starting or running.
196    AlreadyRunning,
197    /// The worker thread could not be created.
198    ThreadSpawnFailed,
199    /// The worker thread panicked before it could be joined cleanly.
200    ThreadPanicked,
201    /// A prior lifecycle operation poisoned the lifecycle mutex.
202    LifecyclePoisoned,
203}
204
205/// Returns whether the worker has entered its running state.
206///
207/// # Parameters
208///
209/// - `state`: Input consumed by [`crate::runtime::job::job_worker_is_running`].
210///
211/// # Returns
212///
213/// Returns `true` when the condition checked by [`crate::runtime::job::job_worker_is_running`] holds and `false` otherwise.
214///
215/// # Examples
216///
217/// ```rust,ignore
218/// let _result = pharos_native::runtime::job::job_worker_is_running(todo!());
219/// ```
220///
221/// # Related
222///
223/// - [`crate::runtime::job`] for adjacent helpers that share the same runtime surface.
224pub fn job_worker_is_running(state: &JobWorkerState) -> bool {
225    state.running.load(Ordering::SeqCst)
226}
227
228#[cfg(not(target_os = "wasi"))]
229/// Makes an owned Rust copy of an optional C-style string value.
230///
231/// # Parameters
232///
233/// - `value`: Input consumed by [`crate::runtime::job::job_worker_strdup_local`].
234///
235/// # Returns
236///
237/// Returns [`Some`] when [`crate::runtime::job::job_worker_strdup_local`] can produce a value and [`None`] otherwise.
238///
239/// # Examples
240///
241/// ```rust,ignore
242/// let _result = pharos_native::runtime::job::job_worker_strdup_local("value");
243/// ```
244///
245/// # Related
246///
247/// - [`crate::runtime::job`] for adjacent helpers that share the same runtime surface.
248pub fn job_worker_strdup_local(value: Option<&str>) -> Option<String> {
249    value.map(ToOwned::to_owned)
250}
251
252#[cfg(not(target_os = "wasi"))]
253/// Runs one bounded durable job-dispatch poll operation.
254///
255/// # Parameters
256///
257/// - `state`: Input consumed by [`crate::runtime::job::job_worker_poll_once`].
258///
259/// # Returns
260///
261/// Returns after [`crate::runtime::job::job_worker_poll_once`] applies its in-place effect.
262///
263/// # Examples
264///
265/// ```rust,ignore
266/// let _result = pharos_native::runtime::job::job_worker_poll_once(todo!());
267/// ```
268///
269/// # Related
270///
271/// - [`crate::runtime::job`] for adjacent helpers that share the same runtime surface.
272pub fn job_worker_poll_once(state: &JobWorkerState) {
273    (state.poll_handler)();
274    state.poll_count.fetch_add(1, Ordering::SeqCst);
275}
276
277#[cfg(not(target_os = "wasi"))]
278/// Sleeps in bounded slices and reports whether shutdown was not requested.
279///
280/// # Parameters
281///
282/// - `state`: Input consumed by [`crate::runtime::job::job_worker_sleep_or_shutdown`].
283/// - `milliseconds`: Input consumed by [`crate::runtime::job::job_worker_sleep_or_shutdown`].
284///
285/// # Returns
286///
287/// Returns `true` when the condition checked by [`crate::runtime::job::job_worker_sleep_or_shutdown`] holds and `false` otherwise.
288///
289/// # Examples
290///
291/// ```rust,ignore
292/// let _result = pharos_native::runtime::job::job_worker_sleep_or_shutdown(todo!(), todo!());
293/// ```
294///
295/// # Related
296///
297/// - [`crate::runtime::job`] for adjacent helpers that share the same runtime surface.
298pub fn job_worker_sleep_or_shutdown(state: &JobWorkerState, milliseconds: u64) -> bool {
299    if state.shutdown.load(Ordering::SeqCst) {
300        return false;
301    }
302    if milliseconds == 0 {
303        thread::yield_now();
304        return !state.shutdown.load(Ordering::SeqCst);
305    }
306
307    let mut remaining = Duration::from_millis(milliseconds);
308    while !remaining.is_zero() {
309        if state.shutdown.load(Ordering::SeqCst) {
310            return false;
311        }
312        let slice = remaining.min(Duration::from_millis(100));
313        let started = std::time::Instant::now();
314        thread::sleep(slice);
315        let elapsed = started.elapsed();
316        remaining = remaining.saturating_sub(elapsed);
317    }
318    !state.shutdown.load(Ordering::SeqCst)
319}
320
321#[cfg(not(target_os = "wasi"))]
322/// Runs the worker until shutdown or the optional completed-poll limit.
323///
324/// # Parameters
325///
326/// - `state`: Input consumed by [`crate::runtime::job::job_worker_thread_main`].
327/// - `max_polls`: Input consumed by [`crate::runtime::job::job_worker_thread_main`].
328///
329/// # Returns
330///
331/// Returns after [`crate::runtime::job::job_worker_thread_main`] applies its in-place effect.
332///
333/// # Examples
334///
335/// ```rust,ignore
336/// let _result = pharos_native::runtime::job::job_worker_thread_main(todo!(), todo!());
337/// ```
338///
339/// # Related
340///
341/// - [`crate::runtime::job`] for adjacent helpers that share the same runtime surface.
342pub fn job_worker_thread_main(state: JobWorkerState, max_polls: Option<usize>) {
343    if state.shutdown.load(Ordering::SeqCst) {
344        state.running.store(false, Ordering::SeqCst);
345        state.lifecycle.store(WORKER_STOPPED, Ordering::SeqCst);
346        return;
347    }
348
349    let entered_running = state
350        .lifecycle
351        .compare_exchange(
352            WORKER_STARTING,
353            WORKER_RUNNING,
354            Ordering::SeqCst,
355            Ordering::SeqCst,
356        )
357        .is_ok()
358        || state
359            .lifecycle
360            .compare_exchange(
361                WORKER_STOPPED,
362                WORKER_RUNNING,
363                Ordering::SeqCst,
364                Ordering::SeqCst,
365            )
366            .is_ok();
367    if !entered_running || state.shutdown.load(Ordering::SeqCst) {
368        state.running.store(false, Ordering::SeqCst);
369        state.lifecycle.store(WORKER_STOPPED, Ordering::SeqCst);
370        return;
371    }
372
373    state.running.store(true, Ordering::SeqCst);
374    while !state.shutdown.load(Ordering::SeqCst) {
375        if max_polls.is_some_and(|limit| state.poll_count() >= limit) {
376            break;
377        }
378        job_worker_poll_once(&state);
379        if max_polls.is_some_and(|limit| state.poll_count() >= limit) {
380            break;
381        }
382        if !job_worker_sleep_or_shutdown(&state, state.poll_interval_ms) {
383            break;
384        }
385    }
386
387    state.running.store(false, Ordering::SeqCst);
388    state.lifecycle.store(WORKER_STOPPED, Ordering::SeqCst);
389}
390
391#[cfg(not(target_os = "wasi"))]
392/// Starts one managed worker thread for the supplied state.
393///
394/// # Parameters
395///
396/// - `state`: Input consumed by [`crate::runtime::job::job_worker_start`].
397/// - `max_polls`: Input consumed by [`crate::runtime::job::job_worker_start`].
398/// - `) -> Result<(`: Input consumed by [`crate::runtime::job::job_worker_start`].
399///
400/// # Returns
401///
402/// Returns the successful value produced by [`crate::runtime::job::job_worker_start`] or an error when the operation fails.
403///
404/// # Errors
405///
406/// Returns an error value when [`crate::runtime::job::job_worker_start`] cannot complete successfully.
407///
408/// # Examples
409///
410/// ```rust,ignore
411/// let _result = pharos_native::runtime::job::job_worker_start(todo!(), todo!(), todo!());
412/// ```
413///
414/// # Related
415///
416/// - [`crate::runtime::job`] for adjacent helpers that share the same runtime surface.
417pub fn job_worker_start(
418    state: &JobWorkerState,
419    max_polls: Option<usize>,
420) -> Result<(), JobWorkerLifecycleError> {
421    let mut thread_slot = state
422        .thread
423        .lock()
424        .map_err(|_| JobWorkerLifecycleError::LifecyclePoisoned)?;
425    if thread_slot.is_some()
426        || state
427            .lifecycle
428            .compare_exchange(
429                WORKER_STOPPED,
430                WORKER_STARTING,
431                Ordering::SeqCst,
432                Ordering::SeqCst,
433            )
434            .is_err()
435    {
436        return Err(JobWorkerLifecycleError::AlreadyRunning);
437    }
438
439    state.shutdown.store(false, Ordering::SeqCst);
440    state.running.store(true, Ordering::SeqCst);
441    let worker_state = state.clone();
442    let spawned = thread::Builder::new()
443        .name("pharos-job-worker".to_owned())
444        .spawn(move || job_worker_thread_main(worker_state, max_polls));
445    let handle = match spawned {
446        Ok(handle) => handle,
447        Err(_) => {
448            state.running.store(false, Ordering::SeqCst);
449            state.lifecycle.store(WORKER_STOPPED, Ordering::SeqCst);
450            return Err(JobWorkerLifecycleError::ThreadSpawnFailed);
451        }
452    };
453
454    *thread_slot = Some(handle);
455    Ok(())
456}
457
458#[cfg(not(target_os = "wasi"))]
459/// Signals a managed worker, joins it, and releases its owned thread handle.
460///
461/// # Parameters
462///
463/// - `state`: Input consumed by [`crate::runtime::job::job_worker_stop`].
464///
465/// # Returns
466///
467/// Returns the successful value produced by [`crate::runtime::job::job_worker_stop`] or an error when the operation fails.
468///
469/// # Errors
470///
471/// Returns an error value when [`crate::runtime::job::job_worker_stop`] cannot complete successfully.
472///
473/// # Examples
474///
475/// ```rust,ignore
476/// let _result = pharos_native::runtime::job::job_worker_stop(todo!());
477/// ```
478///
479/// # Related
480///
481/// - [`crate::runtime::job`] for adjacent helpers that share the same runtime surface.
482pub fn job_worker_stop(state: &JobWorkerState) -> Result<(), JobWorkerLifecycleError> {
483    let handle = {
484        let mut thread_slot = state
485            .thread
486            .lock()
487            .map_err(|_| JobWorkerLifecycleError::LifecyclePoisoned)?;
488        if thread_slot.is_none() && !job_worker_is_running(state) {
489            state.shutdown.store(false, Ordering::SeqCst);
490            state.lifecycle.store(WORKER_STOPPED, Ordering::SeqCst);
491            return Ok(());
492        }
493        state.shutdown.store(true, Ordering::SeqCst);
494        state.lifecycle.store(WORKER_STOPPING, Ordering::SeqCst);
495        thread_slot.take()
496    };
497
498    let join_result = handle.map(|worker| worker.join());
499    state.running.store(false, Ordering::SeqCst);
500    state.shutdown.store(false, Ordering::SeqCst);
501    state.lifecycle.store(WORKER_STOPPED, Ordering::SeqCst);
502    match join_result {
503        Some(Ok(())) | None => Ok(()),
504        Some(Err(_)) => Err(JobWorkerLifecycleError::ThreadPanicked),
505    }
506}