Skip to main content

folk_runtime_embed/
runtime.rs

1//! `Runtime` implementation for embedded PHP.
2//!
3//! Spawns dedicated OS threads, each with its own PHP interpreter.
4//! Optionally warms `OPcache` on a master thread before spawning workers.
5
6use anyhow::Result;
7use async_trait::async_trait;
8use folk_core::runtime::{Runtime, WorkerHandle};
9use tracing::{debug, info};
10
11use crate::handle::EmbedWorkerHandle;
12use crate::php::PhpInstance;
13use crate::worker::spawn_worker_thread;
14
15/// Configuration for the embed runtime.
16#[derive(Debug, Clone)]
17pub struct EmbedConfig {
18    /// Optional PHP bootstrap script to load on each worker.
19    /// For Laravel: `vendor/bin/folk-worker` (same as pipe/fork).
20    pub script: Option<String>,
21
22    /// Files to preload into `OPcache` during warmup.
23    /// Typically `["vendor/autoload.php"]` for Composer projects.
24    /// If empty, no warmup phase runs.
25    pub warmup_files: Vec<String>,
26}
27
28/// Embedded PHP runtime — spawns worker threads instead of processes.
29pub struct EmbedRuntime {
30    config: EmbedConfig,
31    warmup_done: bool,
32}
33
34impl EmbedRuntime {
35    pub fn new(config: EmbedConfig) -> Self {
36        Self {
37            config,
38            warmup_done: false,
39        }
40    }
41
42    /// Run the `OPcache` warmup phase on a dedicated thread.
43    ///
44    /// Boots a temporary PHP instance, loads the specified files
45    /// (triggering `OPcache` compilation into SHM), then shuts down.
46    /// Worker threads spawned after this inherit warm bytecode.
47    pub async fn warmup(&mut self) -> Result<()> {
48        if self.config.warmup_files.is_empty() {
49            debug!("no warmup files configured, skipping `OPcache` warmup");
50            self.warmup_done = true;
51            return Ok(());
52        }
53
54        let files = self.config.warmup_files.clone();
55
56        let result = tokio::task::spawn_blocking(move || warmup_opcache(&files)).await?;
57
58        self.warmup_done = true;
59        result
60    }
61}
62
63#[async_trait]
64impl Runtime for EmbedRuntime {
65    async fn spawn(&self) -> Result<Box<dyn WorkerHandle>> {
66        if !self.warmup_done && !self.config.warmup_files.is_empty() {
67            debug!("`OPcache` warmup not run yet — workers will compile on first request");
68        }
69
70        let (thread, cmd_tx, task_resp_rx, control_rx, worker_id) =
71            spawn_worker_thread(self.config.script.clone());
72
73        Ok(Box::new(EmbedWorkerHandle::new(
74            worker_id,
75            cmd_tx,
76            task_resp_rx,
77            control_rx,
78            thread,
79        )))
80    }
81}
82
83/// Runs on a blocking thread: boots PHP, loads files to warm `OPcache`.
84fn warmup_opcache(files: &[String]) -> Result<()> {
85    info!(files = ?files, "starting `OPcache` warmup");
86
87    let mut php = PhpInstance::boot_custom_sapi()?;
88
89    php.request_startup()?;
90
91    for file in files {
92        debug!(file, "warming `OPcache`");
93        // require_once loads and compiles the file → `OPcache` stores bytecode in SHM
94        let code = format!("require_once '{}';", file.replace('\'', "\\'"));
95        if let Err(e) = php.eval(&code) {
96            tracing::warn!(file, error = ?e, "warmup file failed (non-fatal)");
97        }
98    }
99
100    // Check `OPcache` status
101    let status = php.eval(
102        "if (function_exists('opcache_get_status')) { \
103            $s = opcache_get_status(false); \
104            echo json_encode(['cached_scripts' => $s['opcache_statistics']['num_cached_scripts'] ?? 0, \
105                'hits' => $s['opcache_statistics']['hits'] ?? 0, \
106                'memory_used' => $s['memory_usage']['used_memory'] ?? 0]); \
107        } else { echo '{}'; }",
108    );
109
110    if let Ok(result) = status {
111        info!(opcache_status = %result.output, "`OPcache` warmup complete");
112    }
113
114    php.request_shutdown();
115    // PhpInstance::drop() shuts down PHP but `OPcache` SHM persists in-process
116
117    info!("`OPcache` warmup finished — SHM bytecode available for workers");
118    Ok(())
119}