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    /// Whether this worker can be recycled (terminated and respawned).
38    /// Returns `false` for the main thread worker which cannot be restarted.
39    fn is_recyclable(&self) -> bool {
40        true
41    }
42}
43
44/// Spawns workers per a runtime-specific strategy.
45#[async_trait]
46pub trait Runtime: Send + Sync + 'static {
47    /// Spawn a single worker and return a handle.
48    ///
49    /// The caller must call `ready()` before dispatching requests.
50    async fn spawn(&self) -> Result<Box<dyn WorkerHandle>>;
51
52    /// Warm up shared caches (OPcache) before spawning workers.
53    /// Called once at startup. Default: no-op.
54    async fn warmup(&self) -> Result<()> {
55        Ok(())
56    }
57
58    /// Invalidate compiled-code caches so respawned workers pick up changes.
59    ///
60    /// Called by the dev-mode file watcher just before workers are recycled.
61    /// In ZTS the OPcache is shared process-wide, so a single reset affects
62    /// every worker thread. Default: no-op.
63    async fn reload(&self) -> Result<()> {
64        Ok(())
65    }
66}
67
68// --- MockRuntime: in-memory runtime for tests ---
69
70type MockResponder =
71    std::sync::Arc<dyn Fn(&str, &serde_json::Value) -> Result<serde_json::Value> + Send + Sync>;
72
73/// In-memory runtime used in tests. Each spawned worker echoes requests back.
74pub struct MockRuntime {
75    responder: MockResponder,
76    next_id: std::sync::atomic::AtomicU32,
77}
78
79impl MockRuntime {
80    /// Create a mock runtime that echoes the payload back as the result.
81    pub fn echo() -> Self {
82        Self {
83            responder: std::sync::Arc::new(|_method, payload| Ok(payload.clone())),
84            next_id: std::sync::atomic::AtomicU32::new(10000),
85        }
86    }
87
88    /// Number of workers spawned so far. Useful for asserting recycling.
89    pub fn spawn_count(&self) -> u32 {
90        self.next_id.load(std::sync::atomic::Ordering::Relaxed) - 10000
91    }
92}
93
94#[async_trait]
95impl Runtime for MockRuntime {
96    async fn spawn(&self) -> Result<Box<dyn WorkerHandle>> {
97        let id = self
98            .next_id
99            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
100        Ok(Box::new(MockWorker {
101            id,
102            responder: self.responder.clone(),
103            terminated: false,
104        }))
105    }
106}
107
108/// In-memory worker used by `MockRuntime`.
109pub struct MockWorker {
110    id: u32,
111    responder: MockResponder,
112    terminated: bool,
113}
114
115#[async_trait]
116impl WorkerHandle for MockWorker {
117    fn id(&self) -> u32 {
118        self.id
119    }
120
121    async fn ready(&mut self) -> Result<()> {
122        Ok(())
123    }
124
125    async fn execute(
126        &mut self,
127        method: &str,
128        payload: serde_json::Value,
129    ) -> Result<serde_json::Value> {
130        if self.terminated {
131            anyhow::bail!("worker terminated");
132        }
133        (self.responder)(method, &payload)
134    }
135
136    async fn terminate(&mut self) -> Result<()> {
137        self.terminated = true;
138        Ok(())
139    }
140}