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
59// --- MockRuntime: in-memory runtime for tests ---
60
61type MockResponder =
62 std::sync::Arc<dyn Fn(&str, &serde_json::Value) -> Result<serde_json::Value> + Send + Sync>;
63
64/// In-memory runtime used in tests. Each spawned worker echoes requests back.
65pub struct MockRuntime {
66 responder: MockResponder,
67 next_id: std::sync::atomic::AtomicU32,
68}
69
70impl MockRuntime {
71 /// Create a mock runtime that echoes the payload back as the result.
72 pub fn echo() -> Self {
73 Self {
74 responder: std::sync::Arc::new(|_method, payload| Ok(payload.clone())),
75 next_id: std::sync::atomic::AtomicU32::new(10000),
76 }
77 }
78}
79
80#[async_trait]
81impl Runtime for MockRuntime {
82 async fn spawn(&self) -> Result<Box<dyn WorkerHandle>> {
83 let id = self
84 .next_id
85 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
86 Ok(Box::new(MockWorker {
87 id,
88 responder: self.responder.clone(),
89 terminated: false,
90 }))
91 }
92}
93
94/// In-memory worker used by `MockRuntime`.
95pub struct MockWorker {
96 id: u32,
97 responder: MockResponder,
98 terminated: bool,
99}
100
101#[async_trait]
102impl WorkerHandle for MockWorker {
103 fn id(&self) -> u32 {
104 self.id
105 }
106
107 async fn ready(&mut self) -> Result<()> {
108 Ok(())
109 }
110
111 async fn execute(
112 &mut self,
113 method: &str,
114 payload: serde_json::Value,
115 ) -> Result<serde_json::Value> {
116 if self.terminated {
117 anyhow::bail!("worker terminated");
118 }
119 (self.responder)(method, &payload)
120 }
121
122 async fn terminate(&mut self) -> Result<()> {
123 self.terminated = true;
124 Ok(())
125 }
126}