Skip to main content

folk_core/
runtime.rs

1//! Abstraction over how PHP workers are spawned.
2//!
3//! `WorkerPool` does not know what a PHP process is. It asks the
4//! `Runtime` to spawn a worker; the runtime returns a `WorkerHandle` which
5//! gives the pool a way to execute requests and to terminate the worker.
6//!
7//! In phase 23 (extension mode), the runtime spawns OS threads that run PHP
8//! inside the same process. Communication is via channels (zero IPC).
9
10use anyhow::Result;
11use async_trait::async_trait;
12
13/// A handle to a spawned worker.
14///
15/// The pool dispatches requests via `execute` and controls lifecycle via
16/// `ready` and `terminate`.
17#[async_trait]
18pub trait WorkerHandle: Send + 'static {
19    /// Worker identifier (thread ID, PID, or synthetic).
20    fn id(&self) -> u32;
21
22    /// Wait for the worker to signal readiness.
23    /// Returns once the worker has booted and is ready to accept requests.
24    async fn ready(&mut self) -> Result<()>;
25
26    /// Execute a single request: send structured data, receive result.
27    async fn execute(
28        &mut self,
29        method: &str,
30        payload: serde_json::Value,
31    ) -> Result<serde_json::Value>;
32
33    /// Terminate the worker. Implementations should signal shutdown and
34    /// wait for the worker to exit.
35    async fn terminate(&mut self) -> Result<()>;
36}
37
38/// Spawns workers per a runtime-specific strategy.
39#[async_trait]
40pub trait Runtime: Send + Sync + 'static {
41    /// Spawn a single worker and return a handle.
42    ///
43    /// The caller must call `ready()` before dispatching requests.
44    async fn spawn(&self) -> Result<Box<dyn WorkerHandle>>;
45}
46
47// --- MockRuntime: in-memory runtime for tests ---
48
49type MockResponder =
50    std::sync::Arc<dyn Fn(&str, &serde_json::Value) -> Result<serde_json::Value> + Send + Sync>;
51
52/// In-memory runtime used in tests. Each spawned worker echoes requests back.
53pub struct MockRuntime {
54    responder: MockResponder,
55    next_id: std::sync::atomic::AtomicU32,
56}
57
58impl MockRuntime {
59    /// Create a mock runtime that echoes the payload back as the result.
60    pub fn echo() -> Self {
61        Self {
62            responder: std::sync::Arc::new(|_method, payload| Ok(payload.clone())),
63            next_id: std::sync::atomic::AtomicU32::new(10000),
64        }
65    }
66}
67
68#[async_trait]
69impl Runtime for MockRuntime {
70    async fn spawn(&self) -> Result<Box<dyn WorkerHandle>> {
71        let id = self
72            .next_id
73            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
74        Ok(Box::new(MockWorker {
75            id,
76            responder: self.responder.clone(),
77            terminated: false,
78        }))
79    }
80}
81
82/// In-memory worker used by `MockRuntime`.
83pub struct MockWorker {
84    id: u32,
85    responder: MockResponder,
86    terminated: bool,
87}
88
89#[async_trait]
90impl WorkerHandle for MockWorker {
91    fn id(&self) -> u32 {
92        self.id
93    }
94
95    async fn ready(&mut self) -> Result<()> {
96        Ok(())
97    }
98
99    async fn execute(
100        &mut self,
101        method: &str,
102        payload: serde_json::Value,
103    ) -> Result<serde_json::Value> {
104        if self.terminated {
105            anyhow::bail!("worker terminated");
106        }
107        (self.responder)(method, &payload)
108    }
109
110    async fn terminate(&mut self) -> Result<()> {
111        self.terminated = true;
112        Ok(())
113    }
114}