use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use tokio::sync::{Notify, mpsc};
use tracing::{debug, error};
use super::{PHP_JOB_CHANNEL_CAPACITY, monotonic_now_ms, update_max};
use crate::engine::thread::{PhpThreadContext, bind_php_thread};
use crate::engine::{PhpJob, ensure_sapi, run_php_on_thread};
pub(super) struct SapiThread {
pub(super) id: u8,
pub(super) tx: mpsc::Sender<PhpJob>,
pub(super) busy: Arc<AtomicBool>,
pub(super) request_count: Arc<AtomicU64>,
pub(super) last_used_ms: Arc<AtomicU64>,
pub(super) spawned_at_ms: u64,
pub(super) joiner: std::thread::JoinHandle<()>,
pub(super) exited: Arc<AtomicBool>,
}
struct InFlightGuard {
in_flight: Arc<AtomicUsize>,
busy: Arc<AtomicBool>,
busy_count: Arc<AtomicUsize>,
last_used: Arc<AtomicU64>,
slot_freed: Arc<Notify>,
drain_wake: Arc<Notify>,
}
impl InFlightGuard {
fn enter(
counter: Arc<AtomicUsize>,
max_in_flight: Arc<AtomicUsize>,
busy: Arc<AtomicBool>,
busy_count: Arc<AtomicUsize>,
last_used: Arc<AtomicU64>,
slot_freed: Arc<Notify>,
drain_wake: Arc<Notify>,
) -> Self {
let current = counter.fetch_add(1, Ordering::AcqRel) + 1;
update_max(&max_in_flight, current);
Self {
in_flight: counter,
busy,
busy_count,
last_used,
slot_freed,
drain_wake,
}
}
}
impl Drop for InFlightGuard {
fn drop(&mut self) {
self.in_flight.fetch_sub(1, Ordering::AcqRel);
self.busy_count.fetch_sub(1, Ordering::AcqRel);
self.busy.store(false, Ordering::Release);
self.last_used.store(monotonic_now_ms(), Ordering::Release);
self.slot_freed.notify_one();
self.drain_wake.notify_waiters();
}
}
pub(super) fn spawn_sapi_thread(
slot: u8,
in_flight: Arc<AtomicUsize>,
max_in_flight: Arc<AtomicUsize>,
busy_count: Arc<AtomicUsize>,
slot_freed: Arc<Notify>,
drain_wake: Arc<Notify>,
owner: crate::engine::interrupt::ExecutionOwner,
) -> SapiThread {
let (tx, rx) = mpsc::channel::<PhpJob>(PHP_JOB_CHANNEL_CAPACITY);
let busy = Arc::new(AtomicBool::new(false));
let busy_for_thread = Arc::clone(&busy);
let request_count = Arc::new(AtomicU64::new(0));
let request_count_for_thread = Arc::clone(&request_count);
let spawned_at_ms = monotonic_now_ms();
let last_used_ms = Arc::new(AtomicU64::new(spawned_at_ms));
let last_used_for_thread = Arc::clone(&last_used_ms);
let exited = Arc::new(AtomicBool::new(false));
let exited_for_thread = Arc::clone(&exited);
let joiner = std::thread::Builder::new()
.name(format!("claviron-php-{slot}"))
.spawn(move || {
php_thread_main(
slot,
rx,
in_flight,
max_in_flight,
busy_count,
busy_for_thread,
request_count_for_thread,
last_used_for_thread,
slot_freed,
Arc::clone(&drain_wake),
owner,
);
exited_for_thread.store(true, Ordering::Release);
drain_wake.notify_waiters();
})
.expect("spawn claviron-php thread");
debug!("🧵 spawned claviron-sapi thread {slot}");
#[cfg(feature = "metrics")]
crate::metrics::record_thread_spawned();
SapiThread {
id: slot,
tx,
busy,
request_count,
last_used_ms,
spawned_at_ms,
joiner,
exited,
}
}
#[allow(clippy::too_many_arguments)]
fn php_thread_main(
thread_id: u8,
mut rx: mpsc::Receiver<PhpJob>,
in_flight: Arc<AtomicUsize>,
max_in_flight: Arc<AtomicUsize>,
busy_count: Arc<AtomicUsize>,
busy: Arc<AtomicBool>,
request_count: Arc<AtomicU64>,
last_used: Arc<AtomicU64>,
slot_freed: Arc<Notify>,
drain_wake: Arc<Notify>,
owner: crate::engine::interrupt::ExecutionOwner,
) {
if let Err(err) = ensure_sapi() {
error!(thread_id, "php SAPI startup: {err}");
return;
}
let _thread_context = PhpThreadContext;
if let Err(err) = bind_php_thread(thread_id, owner) {
error!(thread_id, "php thread: {err}");
return;
}
while let Some(job) = rx.blocking_recv() {
let _guard = InFlightGuard::enter(
Arc::clone(&in_flight),
Arc::clone(&max_in_flight),
Arc::clone(&busy),
Arc::clone(&busy_count),
Arc::clone(&last_used),
Arc::clone(&slot_freed),
Arc::clone(&drain_wake),
);
let _active = crate::engine::interrupt::Active::enter();
let result = run_php_on_thread(
job.runtime,
job.sapi_req,
job.parts,
job.conn_ctx,
job.request_body,
job.body_sink,
);
let _ = job.reply.send(result);
request_count.fetch_add(1, Ordering::Relaxed);
}
debug!(thread_id, "claviron-php thread: channel closed, exiting");
#[cfg(feature = "metrics")]
crate::metrics::record_thread_exited();
}