#![cfg_attr(not(feature = "sqlite"), allow(dead_code))]
use std::cell::{Cell, RefCell};
use std::sync::{Arc, Condvar, Mutex, PoisonError};
use std::time::Duration;
const GIVE_UP: Duration = Duration::from_secs(2);
#[derive(Default)]
pub struct Gate {
state: Mutex<GateState>,
turn: Condvar,
}
#[derive(Default)]
struct GateState {
parked: bool,
parks: u64,
done_in: u64,
used: bool,
sims: u32,
open: u32,
give_ups: u64,
}
impl Gate {
fn on_park(&self) {
let mut state = self.lock();
state.parked = true;
state.parks += 1;
state.used = false;
drop(state);
self.turn.notify_all();
}
fn on_unpark(&self) {
self.lock().parked = false;
}
fn lock(&self) -> std::sync::MutexGuard<'_, GateState> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
fn take_turn(&self) {
let mut state = self.lock();
let deadline = std::time::Instant::now() + GIVE_UP;
while !(state.parked && !state.used && state.parks > state.done_in)
&& state.sims > 0
&& state.open == 0
{
let left = deadline.saturating_duration_since(std::time::Instant::now());
if left.is_zero() {
state.give_ups += 1;
tracing::warn!(
"sim gate: the runtime did not park for {GIVE_UP:?}; a query runs \
without its turn (a task dropped a query in flight?)"
);
break;
}
state = self
.turn
.wait_timeout(state, left)
.unwrap_or_else(PoisonError::into_inner)
.0;
}
state.used = true;
}
fn end_turn(&self) {
let mut state = self.lock();
state.done_in = state.parks;
}
}
thread_local! {
static GATE: RefCell<Option<Arc<Gate>>> = const { RefCell::new(None) };
static RUNS: RefCell<Option<Arc<Gate>>> = const { RefCell::new(None) };
static DEPTH: Cell<u32> = const { Cell::new(0) };
}
pub fn enter() {
let depth = DEPTH.with(Cell::get);
if depth == 0 {
let gate = GATE.with(|gate| gate.borrow().clone());
if let Some(gate) = gate {
gate.take_turn();
}
}
DEPTH.with(|cell| cell.set(depth.saturating_add(1)));
}
pub fn exit() {
let depth = DEPTH.with(|depth| {
let left = depth.get().saturating_sub(1);
depth.set(left);
left
});
if depth == 0
&& let Some(gate) = GATE.with(|gate| gate.borrow().clone())
{
gate.end_turn();
}
}
#[cfg(test)]
fn live_sims() -> u32 {
runtime_gate().map_or(0, |gate| gate.lock().sims)
}
fn runtime_gate() -> Option<Arc<Gate>> {
RUNS.with(|gate| gate.borrow().clone())
}
pub fn give_ups() -> u64 {
runtime_gate().map_or(0, |gate| gate.lock().give_ups)
}
#[derive(Default)]
pub struct SimSeat(Mutex<Option<Arc<Gate>>>);
impl SimSeat {
pub fn take(&self) {
let Some(gate) = runtime_gate() else {
return;
};
let mut seat = self.0.lock().unwrap_or_else(PoisonError::into_inner);
if seat.as_ref().is_some_and(|held| Arc::ptr_eq(held, &gate)) {
return;
}
gate.lock().sims += 1;
let old = seat.replace(gate);
drop(seat);
if let Some(old) = old {
Self::release(&old);
}
}
fn release(gate: &Gate) {
let mut state = gate.lock();
state.sims = state.sims.saturating_sub(1);
drop(state);
gate.turn.notify_all();
}
pub fn leave(&self) {
let gate = self.0.lock().unwrap_or_else(PoisonError::into_inner).take();
if let Some(gate) = gate {
Self::release(&gate);
}
}
}
impl Drop for SimSeat {
fn drop(&mut self) {
self.leave();
}
}
pub struct Open(Option<Arc<Gate>>);
impl Open {
pub fn new() -> Self {
let gate = runtime_gate();
if let Some(gate) = &gate {
gate.lock().open += 1;
gate.turn.notify_all();
}
Self(gate)
}
}
impl Drop for Open {
fn drop(&mut self) {
if let Some(gate) = &self.0 {
let mut state = gate.lock();
state.open = state.open.saturating_sub(1);
}
}
}
pub struct Scope(());
impl Scope {
pub fn enter() -> Self {
enter();
Self(())
}
}
impl Drop for Scope {
fn drop(&mut self) {
exit();
}
}
#[derive(Debug, Default)]
pub struct QueryTurn {
took: bool,
}
impl QueryTurn {
pub fn start(&mut self) {
self.finish();
if DEPTH.with(Cell::get) == 0
&& let Some(gate) = GATE.with(|gate| gate.borrow().clone())
{
gate.take_turn();
self.took = true;
}
}
pub fn finish(&mut self) {
if std::mem::take(&mut self.took)
&& let Some(gate) = GATE.with(|gate| gate.borrow().clone())
{
gate.end_turn();
}
}
}
#[cfg(feature = "db")]
#[derive(Default)]
pub struct GateInstrumentation(QueryTurn);
#[cfg(feature = "db")]
impl diesel::connection::Instrumentation for GateInstrumentation {
fn on_connection_event(&mut self, event: diesel::connection::InstrumentationEvent<'_>) {
use diesel::connection::InstrumentationEvent;
match event {
InstrumentationEvent::StartQuery { .. } => self.0.start(),
InstrumentationEvent::FinishQuery { .. } => self.0.finish(),
_ => {}
}
}
}
pub fn runtime() -> std::io::Result<SimRuntime> {
let gate = Arc::new(Gate::default());
install(&gate);
let on_start = Arc::clone(&gate);
let on_unpark = Arc::clone(&gate);
let on_park = Arc::clone(&gate);
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.start_paused(true)
.max_blocking_threads(1)
.on_thread_start(move || {
GATE.with(|cell| *cell.borrow_mut() = Some(Arc::clone(&on_start)));
})
.on_thread_park(move || {
install(&on_park);
on_park.on_park();
})
.on_thread_unpark(move || on_unpark.on_unpark())
.build()?;
Ok(SimRuntime { runtime, gate })
}
fn install(gate: &Arc<Gate>) {
RUNS.with(|cell| {
let mut cell = cell.borrow_mut();
if !cell.as_ref().is_some_and(|held| Arc::ptr_eq(held, gate)) {
*cell = Some(Arc::clone(gate));
}
});
}
pub struct SimRuntime {
runtime: tokio::runtime::Runtime,
gate: Arc<Gate>,
}
impl SimRuntime {
pub fn block_on<F: std::future::Future>(&self, future: F) -> F::Output {
install(&self.gate);
self.runtime.block_on(future)
}
pub fn shutdown_timeout(self, duration: Duration) {
self.runtime.shutdown_timeout(duration);
}
pub fn shutdown_background(self) {
self.runtime.shutdown_background();
}
}
impl std::ops::Deref for SimRuntime {
type Target = tokio::runtime::Runtime;
fn deref(&self) -> &Self::Target {
&self.runtime
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use super::{Scope, SimSeat, runtime};
#[test]
fn sim_gate_gated_work_ends_after_local_work() {
for _ in 0..20 {
let order = Arc::new(Mutex::new(Vec::new()));
let rt = runtime().unwrap();
let seat = SimSeat::default();
seat.take();
let seen = Arc::clone(&order);
rt.block_on(async move {
let log = Arc::clone(&seen);
let blocking = crate::time::spawn_blocking(move || {
let _gate = Scope::enter();
log.lock().unwrap().push("blocking");
});
for _ in 0..3 {
seen.lock().unwrap().push("local");
tokio::task::yield_now().await;
}
blocking.await.unwrap();
});
seat.leave();
assert_eq!(
*order.lock().unwrap(),
["local", "local", "local", "blocking"]
);
}
}
#[test]
fn sim_gate_one_operation_per_park_and_nesting_passes() {
let rt = runtime().unwrap();
let seat = SimSeat::default();
seat.take();
let done = Arc::new(AtomicUsize::new(0));
rt.block_on(async {
let mut handles = Vec::new();
for _ in 0..4 {
let done = Arc::clone(&done);
handles.push(crate::time::spawn_blocking(move || {
let _outer = Scope::enter();
let _inner = Scope::enter();
done.fetch_add(1, Ordering::SeqCst);
}));
}
for handle in handles {
handle.await.unwrap();
}
});
seat.leave();
assert_eq!(done.load(Ordering::SeqCst), 4);
}
#[test]
fn sim_gate_does_not_wait_with_no_sim_or_while_open() {
let rt = runtime().unwrap();
rt.block_on(async {
let handle = crate::time::spawn_blocking(|| {
let _gate = Scope::enter();
});
while !handle.is_finished() {
tokio::task::yield_now().await;
}
});
let seat = SimSeat::default();
seat.take();
rt.block_on(async {
let _open = super::Open::new();
let handle = crate::time::spawn_blocking(|| {
let _gate = Scope::enter();
});
while !handle.is_finished() {
tokio::task::yield_now().await;
}
});
seat.leave();
}
#[test]
fn sim_gate_a_query_turn_left_open_does_not_leak() {
{
let mut turn = super::QueryTurn::default();
turn.start();
}
assert_eq!(super::DEPTH.with(std::cell::Cell::get), 0);
}
#[test]
fn sim_gate_counts_a_sim_built_before_its_runtime_once_anchored() {
let sim = crate::sim::Sim::from_seed(1);
let rt = runtime().unwrap();
rt.block_on(async {
sim.anchor();
assert_eq!(super::live_sims(), 1, "the anchored sim counts");
sim.anchor();
assert_eq!(super::live_sims(), 1, "a second anchor counts nothing");
let inner = crate::sim::Sim::from_seed(2);
assert_eq!(super::live_sims(), 2);
drop(inner);
assert_eq!(super::live_sims(), 1);
drop(sim);
assert_eq!(super::live_sims(), 0);
});
}
#[test]
fn sim_gate_moves_a_sim_to_the_gate_of_a_new_runtime() {
drop(runtime().unwrap());
let sim = crate::sim::Sim::from_seed(1);
let rt = runtime().unwrap();
rt.block_on(async {
sim.anchor();
assert_eq!(super::live_sims(), 1, "the sim counts on the new gate");
drop(sim);
assert_eq!(super::live_sims(), 0);
});
}
#[test]
fn sim_gate_seats_a_sim_when_the_runtime_runs_on_another_thread() {
let rt = runtime().unwrap();
let seats = std::thread::spawn(move || {
rt.block_on(async {
let sim = crate::sim::Sim::from_seed(1);
sim.anchor();
super::live_sims()
})
})
.join()
.unwrap();
assert_eq!(seats, 1);
}
#[test]
fn sim_gate_gates_work_when_the_runtime_runs_on_another_thread() {
let rt = runtime().unwrap();
let order = std::thread::spawn(move || {
let order = Arc::new(Mutex::new(Vec::new()));
let seen = Arc::clone(&order);
rt.block_on(async move {
let sim = crate::sim::Sim::from_seed(1);
sim.anchor();
let log = Arc::clone(&seen);
let blocking = crate::time::spawn_blocking(move || {
let _gate = Scope::enter();
log.lock().unwrap().push("blocking");
});
for _ in 0..3 {
seen.lock().unwrap().push("local");
tokio::task::yield_now().await;
}
blocking.await.unwrap();
});
order.lock().unwrap().clone()
})
.join()
.unwrap();
assert_eq!(order, ["local", "local", "local", "blocking"]);
}
#[test]
fn sim_gate_runtime_derefs_and_shuts_down() {
let rt = runtime().unwrap();
let out = rt.handle().block_on(async { 7 });
assert_eq!(out, 7);
let task = rt.spawn(async { 1 });
assert_eq!(rt.block_on(task).unwrap(), 1);
rt.shutdown_timeout(std::time::Duration::from_millis(10));
runtime().unwrap().shutdown_background();
}
#[test]
fn sim_gate_off_a_sim_runtime_does_nothing() {
let _scope = Scope::enter();
let _again = Scope::enter();
}
}