use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::value::{DictMap, VmError, VmValue};
static TURN_EPOCH: AtomicU64 = AtomicU64::new(0);
thread_local! {
static TURN_STABLE_HOST_CACHE: RefCell<HashMap<String, (u64, VmValue)>> =
RefCell::new(HashMap::new());
}
fn current_epoch() -> u64 {
TURN_EPOCH.load(Ordering::Acquire)
}
fn is_turn_stable(capability: &str, operation: &str) -> bool {
matches!((capability, operation), ("runtime", "pipeline_input"))
}
fn cache_key(capability: &str, operation: &str, params: &DictMap) -> String {
if params.is_empty() {
return format!("{capability}.{operation}");
}
let json = crate::llm::helpers::vm_value_to_json(&VmValue::dict(params.clone()));
format!(
"{capability}.{operation}#{}",
serde_json::to_string(&json).unwrap_or_default()
)
}
pub(crate) async fn cached_or<F, Fut>(
capability: &str,
operation: &str,
params: &DictMap,
dispatch: F,
) -> Result<Option<VmValue>, VmError>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<Option<VmValue>, VmError>>,
{
if !is_turn_stable(capability, operation) {
return dispatch().await;
}
if let Some(cached) = lookup(capability, operation, params) {
return Ok(Some(cached));
}
let result = dispatch().await?;
if let Some(value) = &result {
store(capability, operation, params, value);
}
Ok(result)
}
pub fn lookup(capability: &str, operation: &str, params: &DictMap) -> Option<VmValue> {
if !is_turn_stable(capability, operation) {
return None;
}
let key = cache_key(capability, operation, params);
let epoch = current_epoch();
TURN_STABLE_HOST_CACHE.with(|cache| {
cache
.borrow()
.get(&key)
.filter(|(written, _)| *written == epoch)
.map(|(_, value)| value.clone())
})
}
pub fn store(capability: &str, operation: &str, params: &DictMap, value: &VmValue) {
if !is_turn_stable(capability, operation) {
return;
}
let key = cache_key(capability, operation, params);
let epoch = current_epoch();
TURN_STABLE_HOST_CACHE.with(|cache| {
cache.borrow_mut().insert(key, (epoch, value.clone()));
});
}
pub fn lookup_by_name(name: &str, params: &DictMap) -> Option<VmValue> {
let (capability, operation) = name.split_once('.')?;
lookup(capability, operation, params)
}
pub fn store_by_name(name: &str, params: &DictMap, value: &VmValue) {
if let Some((capability, operation)) = name.split_once('.') {
store(capability, operation, params, value);
}
}
pub(crate) fn reset() {
TURN_EPOCH.fetch_add(1, Ordering::AcqRel);
reset_local();
}
pub(crate) fn reset_local() {
TURN_STABLE_HOST_CACHE.with(|cache| cache.borrow_mut().clear());
}
#[cfg(test)]
pub(crate) fn epoch_test_lock() -> &'static std::sync::Mutex<()> {
static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
LOCK.get_or_init(|| std::sync::Mutex::new(()))
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::super::{
clear_host_call_bridge, dispatch_host_operation, reset_host_state, set_host_call_bridge,
HostCallBridge,
};
use super::reset;
use crate::value::{DictMap, VmValue};
struct CountingRuntimeBridge {
counts: Arc<Mutex<std::collections::HashMap<(String, String), usize>>>,
}
impl HostCallBridge for CountingRuntimeBridge {
fn dispatch<'a>(
&'a self,
capability: &'a str,
operation: &'a str,
_params: &'a DictMap,
) -> super::super::HostCallDispatchFuture<'a> {
*self
.counts
.lock()
.unwrap()
.entry((capability.to_string(), operation.to_string()))
.or_insert(0) += 1;
super::super::host_call_ready(Ok(Some(VmValue::String(arcstr::ArcStr::from(format!(
"{capability}.{operation}"
))))))
}
}
fn run_async<F, Fut>(test: F)
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = ()>,
{
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
rt.block_on(async {
let local = tokio::task::LocalSet::new();
local.run_until(test()).await;
});
}
#[test]
fn turn_stable_host_capability_is_fetched_once_per_turn() {
let _guard = super::epoch_test_lock()
.lock()
.unwrap_or_else(|e| e.into_inner());
run_async(|| async {
reset_host_state();
let counts = Arc::new(Mutex::new(std::collections::HashMap::new()));
set_host_call_bridge(Arc::new(CountingRuntimeBridge {
counts: counts.clone(),
}));
let count = |cap: &str, op: &str| -> usize {
counts
.lock()
.unwrap()
.get(&(cap.to_string(), op.to_string()))
.copied()
.unwrap_or(0)
};
for _ in 0..20 {
dispatch_host_operation("runtime", "pipeline_input", &DictMap::new())
.await
.expect("pipeline_input");
}
assert_eq!(
count("runtime", "pipeline_input"),
1,
"20 same-turn reads must hit the host exactly once"
);
for _ in 0..3 {
dispatch_host_operation("runtime", "record_run", &DictMap::new())
.await
.expect("record_run");
}
assert_eq!(
count("runtime", "record_run"),
3,
"writes/non-stable ops must never be served from the turn memo"
);
reset();
for _ in 0..20 {
dispatch_host_operation("runtime", "pipeline_input", &DictMap::new())
.await
.expect("pipeline_input");
}
assert_eq!(
count("runtime", "pipeline_input"),
2,
"a new turn must re-fetch once, not serve the prior turn's value"
);
clear_host_call_bridge();
});
}
#[test]
fn turn_boundary_on_another_thread_invalidates_this_thread() {
let _guard = super::epoch_test_lock()
.lock()
.unwrap_or_else(|e| e.into_inner());
let params = DictMap::new();
let cached = VmValue::String(arcstr::ArcStr::from("turn-1"));
super::store("runtime", "pipeline_input", ¶ms, &cached);
assert!(
super::lookup("runtime", "pipeline_input", ¶ms).is_some(),
"same-turn read must hit"
);
std::thread::spawn(reset).join().expect("reset thread");
assert!(
super::lookup("runtime", "pipeline_input", ¶ms).is_none(),
"a turn boundary observed on another thread must invalidate this thread's entry"
);
}
#[test]
fn store_ignores_non_turn_stable_operations() {
let _guard = super::epoch_test_lock()
.lock()
.unwrap_or_else(|e| e.into_inner());
let params = DictMap::new();
let value = VmValue::String(arcstr::ArcStr::from("live"));
super::store("session", "active_roots", ¶ms, &value);
assert!(
super::lookup("session", "active_roots", ¶ms).is_none(),
"non-allowlisted reads must never be served from the memo"
);
}
#[test]
fn dotted_name_helpers_share_the_split_pair_entry() {
let _guard = super::epoch_test_lock()
.lock()
.unwrap_or_else(|e| e.into_inner());
reset();
let params = DictMap::new();
let value = VmValue::String(arcstr::ArcStr::from("shared"));
super::store_by_name("runtime.pipeline_input", ¶ms, &value);
assert_eq!(
super::lookup("runtime", "pipeline_input", ¶ms).map(|v| v.display()),
Some("shared".to_string()),
"store_by_name must populate the entry lookup() reads"
);
assert_eq!(
super::lookup_by_name("runtime.pipeline_input", ¶ms).map(|v| v.display()),
Some("shared".to_string()),
"lookup_by_name must read it back"
);
assert!(
super::lookup_by_name("no-separator", ¶ms).is_none(),
"a name without a capability separator must not panic or match"
);
}
}