use std::ffi::c_char;
use std::sync::{Arc, OnceLock};
use ext_php_rs::types::Zval;
use ext_php_rs::zend::{ExecuteData, Function};
use tracing::{debug, info, warn};
static ORIGINAL_APCU_DELETE: OnceLock<
unsafe extern "C" fn(
execute_data: *mut ext_php_rs::ffi::_zend_execute_data,
retval: *mut ext_php_rs::ffi::_zval_struct,
),
> = OnceLock::new();
static APPLY_QUEUE: OnceLock<
std::sync::Mutex<std::collections::VecDeque<crate::ipc::OwnedInvalEvent>>,
> = OnceLock::new();
fn apply_queue() -> &'static std::sync::Mutex<
std::collections::VecDeque<crate::ipc::OwnedInvalEvent>,
> {
APPLY_QUEUE.get_or_init(|| {
std::sync::Mutex::new(std::collections::VecDeque::new())
})
}
pub(crate) fn enqueue_apply(event: crate::ipc::OwnedInvalEvent) {
if let Ok(mut q) = apply_queue().lock() {
if q.contains(&event) {
debug!(target: "claviron::apcu_intercept", "PHP cache invalidation coalesced: {event:?} (already pending)");
} else {
q.push_back(event);
}
}
}
pub(crate) fn drain_apply_queue() {
let drained: Vec<crate::ipc::OwnedInvalEvent> = match apply_queue().lock() {
Ok(mut q) => q.drain(..).collect(),
Err(_) => return,
};
if drained.is_empty() {
return;
}
let Some(apcu_delete) = Function::try_from_function("apcu_delete") else {
warn!(
target: "claviron::apcu_intercept",
count = drained.len(),
"apply: apcu_delete not registered; events dropped"
);
return;
};
for event in drained {
match event {
crate::ipc::OwnedInvalEvent::Delete(key) => {
let key_str = key.as_str();
let result = super::with_replay_guard(|| {
apcu_delete.try_call(vec![&key_str])
});
match result {
Ok(retval) => {
debug!(
target: "claviron::apcu_intercept",
kind = "applied",
key = %key,
success = retval.is_true(),
"broadcast delete applied: apcu_delete key={key} replay=true deleted={}",
retval.is_true()
);
}
Err(e) => {
warn!(
target: "claviron::apcu_intercept",
kind = "applied",
key = %key,
error = %e,
"broadcast delete call failed: apcu_delete key={key} replay=true error={e}"
);
}
}
}
crate::ipc::OwnedInvalEvent::DeletePattern(regex) => {
debug!(
target: "claviron::apcu_intercept",
kind = "applied",
regex = %regex,
"broadcast pattern delete deferred: pattern={regex} (apply not implemented)"
);
}
other => {
warn!(
target: "claviron::apcu_intercept",
event = ?other,
"apcu_delete apply queue: unexpected variant {other:?}; dispatcher bug?"
);
}
}
}
}
extern "C" fn claviron_apcu_delete_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
let captured = capture_apcu_delete_arg(ex);
match ORIGINAL_APCU_DELETE.get() {
Some(original) => unsafe {
original(ex as *mut ExecuteData, retval as *mut Zval);
},
None => {
retval.set_bool(false);
return;
}
}
let replay = super::replay_in_progress();
if !replay && let Some(bus) = crate::bus::bus() {
publish_apcu_events(bus, &captured, retval);
}
match captured {
ApcuDeleteArg::String(key) => {
info!(
target: "claviron::apcu_intercept",
arg = "string",
key = %key,
success = retval.is_true(),
replay,
"apcu_delete intercepted: key={key} replay={replay} deleted={}",
retval.is_true()
);
}
ApcuDeleteArg::Array(keys) => {
let failed: Vec<String> = retval
.array()
.map(|h| h.values().filter_map(|v| v.string()).collect())
.unwrap_or_default();
info!(
target: "claviron::apcu_intercept",
arg = "array",
count = keys.len(),
keys = ?keys,
failed_count = failed.len(),
failed = ?failed,
success = failed.is_empty(),
replay,
"apcu_delete intercepted: keys={keys:?} replay={replay} failed={failed:?}"
);
}
ApcuDeleteArg::Iterator(class) => {
info!(
target: "claviron::apcu_intercept",
arg = "iterator",
class = %class,
success = retval.is_true(),
replay,
"apcu_delete intercepted: iterator={class} replay={replay} success={}",
retval.is_true()
);
}
ApcuDeleteArg::Other(ty) => {
info!(
target: "claviron::apcu_intercept",
arg = "other",
php_type = %ty,
success = retval.is_true(),
replay,
"apcu_delete intercepted: php_type={ty} replay={replay} success={}",
retval.is_true()
);
}
ApcuDeleteArg::None => {
info!(
target: "claviron::apcu_intercept",
arg = "none",
replay,
"apcu_delete intercepted: no args replay={replay}"
);
}
}
}
#[derive(Debug)]
enum ApcuDeleteArg {
String(String),
Array(Vec<String>),
Iterator(String),
Other(String),
None,
}
fn capture_apcu_delete_arg(ex: &mut ExecuteData) -> ApcuDeleteArg {
let zval = unsafe { ex.zend_call_arg(0) };
let Some(zval) = zval else {
return ApcuDeleteArg::None;
};
if zval.is_string() {
return zval
.string()
.map(ApcuDeleteArg::String)
.unwrap_or_else(|| ApcuDeleteArg::Other("string".to_string()));
}
if let Some(arr) = zval.array() {
let keys: Vec<String> =
arr.values().filter_map(|v| v.string()).collect();
return ApcuDeleteArg::Array(keys);
}
if let Some(obj) = zval.object() {
let class =
obj.get_class_name().unwrap_or_else(|_| "<unknown>".to_string());
return ApcuDeleteArg::Iterator(class);
}
ApcuDeleteArg::Other(format!("{:?}", zval.get_type()))
}
fn publish_apcu_events(
bus: &Arc<crate::bus::PhpInvalidationBus>,
captured: &ApcuDeleteArg,
retval: &Zval,
) {
use crate::invalidation::ipc::InvalEvent;
match captured {
ApcuDeleteArg::String(key) => {
if retval.is_true() {
bus.publish(InvalEvent::Delete(key));
}
}
ApcuDeleteArg::Array(keys) => {
let failed: std::collections::HashSet<String> = retval
.array()
.map(|h| h.values().filter_map(|v| v.string()).collect())
.unwrap_or_default();
for key in keys {
if !failed.contains(key) {
bus.publish(InvalEvent::Delete(key));
}
}
}
ApcuDeleteArg::Iterator(class) => {
if retval.is_true() {
bus.publish(InvalEvent::DeletePattern(class));
}
}
ApcuDeleteArg::Other(_) | ApcuDeleteArg::None => {}
}
}
pub(crate) fn ensure_apcu_delete_intercept_for_current_thread() {
use ext_php_rs::ffi::{_zend_function, zend_hash_str_find_ptr_lc};
type FunctionPointerHandler =
extern "C" fn(execute_data: *mut ExecuteData, retval: *mut Zval);
type RawFunctionPointerHandler = unsafe extern "C" fn(
execute_data: *mut ext_php_rs::ffi::_zend_execute_data,
retval: *mut ext_php_rs::ffi::_zval_struct,
);
unsafe {
let cg = ext_php_rs::ffi::ext_php_rs_compiler_globals();
if cg.is_null() {
warn!("apcu_delete intercept: compiler globals null");
return;
}
let function_table_ptr = (*cg).function_table;
if function_table_ptr.is_null() {
warn!("apcu_delete intercept: CG(function_table) is null");
return;
}
let key: &[u8] = b"apcu_delete";
let func_ptr = zend_hash_str_find_ptr_lc(
function_table_ptr,
key.as_ptr() as *const c_char,
key.len(),
) as *mut _zend_function;
if func_ptr.is_null() {
warn!(
"apcu_delete intercept: 'apcu_delete' not in CG(function_table); APCu extension \
not loaded for this thread — skipping observer"
);
return;
}
let our_handler_addr =
claviron_apcu_delete_handler as *const () as usize;
if let Some(current) = (*func_ptr).internal_function.handler {
if current as *const () as usize == our_handler_addr {
return;
}
let _ = ORIGINAL_APCU_DELETE.set(current);
} else {
warn!(
"apcu_delete intercept: 'apcu_delete' has null handler; skipping override"
);
return;
}
let new_handler: FunctionPointerHandler = std::mem::transmute(
claviron_apcu_delete_handler as extern "C" fn(_, _),
);
(*func_ptr).internal_function.handler = Some(std::mem::transmute::<
FunctionPointerHandler,
RawFunctionPointerHandler,
>(new_handler));
#[cfg(debug_assertions)]
tracing::trace!(
"apcu_delete intercept: handler swapped for current thread"
);
}
}