Skip to main content

moq_uring/
timer.rs

1//! Userspace timers: a heap the worker sweeps, never a timeout SQE.
2//!
3//! Every armed deadline lives in one ordered map. The worker fires the due
4//! prefix each turn and parks with the earliest remaining instant as the
5//! absolute `io_uring_enter` timeout, so timers cost zero submissions and
6//! re-arming is a pure memory operation.
7
8use std::cell::{Cell, RefCell};
9use std::collections::BTreeMap;
10use std::rc::Rc;
11use std::sync::Arc;
12use std::task::Poll;
13use std::time::Instant;
14
15use crate::metrics::Counters;
16
17/// The ordered set of armed timers, keyed by (deadline, tiebreak).
18pub(crate) struct Heap {
19	queue: BTreeMap<(Instant, u64), Rc<Slot>>,
20	seq: u64,
21	metrics: Arc<Counters>,
22}
23
24/// One timer's state, shared between its [`Timer`] handle and the heap.
25struct Slot {
26	/// The heap key while armed and unfired.
27	key: Cell<Option<(Instant, u64)>>,
28	/// Fired and not yet re-armed; `poll` stays `Ready` (fused).
29	elapsed: Cell<bool>,
30	waiters: RefCell<kio::WaiterList>,
31}
32
33impl Heap {
34	pub fn new(metrics: Arc<Counters>) -> Self {
35		Self {
36			queue: BTreeMap::new(),
37			seq: 0,
38			metrics,
39		}
40	}
41
42	/// Fire everything due at `now`. Returns whether anything fired.
43	pub fn fire(&mut self, now: Instant) -> bool {
44		let mut fired = false;
45		while let Some(entry) = self.queue.first_entry() {
46			if entry.key().0 > now {
47				break;
48			}
49			let slot = entry.remove();
50			slot.key.set(None);
51			slot.elapsed.set(true);
52			slot.waiters.borrow_mut().wake();
53			self.metrics.timers_fired.add(1);
54			fired = true;
55		}
56		fired
57	}
58
59	/// The earliest armed deadline, if any: the park timeout.
60	pub fn next(&self) -> Option<Instant> {
61		self.queue.first_key_value().map(|(key, _)| key.0)
62	}
63
64	fn insert(&mut self, at: Instant, slot: Rc<Slot>) -> (Instant, u64) {
65		self.seq += 1;
66		let key = (at, self.seq);
67		self.queue.insert(key, slot);
68		self.metrics.timers_armed.add(1);
69		key
70	}
71
72	/// Drop an armed timer before its deadline. A re-arm goes through here
73	/// too, so the counter reads as timer churn rather than as loss.
74	fn cancel(&mut self, key: (Instant, u64)) {
75		if self.queue.remove(&key).is_some() {
76			self.metrics.timers_cancelled.add(1);
77		}
78	}
79
80	/// Drop an armed timer a poll found already due.
81	fn fire_one(&mut self, key: (Instant, u64)) {
82		if self.queue.remove(&key).is_some() {
83			self.metrics.timers_fired.add(1);
84		}
85	}
86}
87
88/// A single re-armable timer slot in a worker's heap.
89///
90/// Arm with [`Self::set`] and poll from this worker's thread.
91/// Created by [`crate::Handle::timer`].
92pub struct Timer {
93	at: Option<Instant>,
94	heap: Rc<RefCell<Heap>>,
95	slot: Rc<Slot>,
96}
97
98impl Timer {
99	pub(crate) fn from_heap(heap: Rc<RefCell<Heap>>) -> Self {
100		Self {
101			at: None,
102			heap,
103			slot: Rc::new(Slot {
104				key: Cell::new(None),
105				elapsed: Cell::new(false),
106				waiters: RefCell::new(kio::WaiterList::new()),
107			}),
108		}
109	}
110}
111
112impl Timer {
113	/// Arm or disarm the timer.
114	pub fn set(&mut self, at: Option<Instant>) {
115		if self.at == at {
116			return;
117		}
118		self.at = at;
119		let mut heap = self.heap.borrow_mut();
120		if let Some(key) = self.slot.key.take() {
121			heap.cancel(key);
122		}
123		self.slot.elapsed.set(false);
124		if let Some(at) = at {
125			// An instant already in the past still goes through the heap: the
126			// worker's next sweep fires it, and a poll before then observes
127			// `at <= now` directly.
128			let key = heap.insert(at, self.slot.clone());
129			self.slot.key.set(Some(key));
130		}
131	}
132
133	/// Poll for expiration and register for wakeups.
134	pub fn poll(&mut self, waiter: &kio::Waiter) -> Poll<()> {
135		if self.slot.elapsed.get() {
136			return Poll::Ready(());
137		}
138		let Some((at, _)) = self.slot.key.get() else {
139			return Poll::Pending;
140		};
141		if at <= Instant::now() {
142			// Fire eagerly rather than waiting for the sweep, and drop the
143			// heap entry so the sweep doesn't wake anyone spuriously.
144			self.heap.borrow_mut().fire_one(self.slot.key.take().expect("armed"));
145			self.slot.elapsed.set(true);
146			return Poll::Ready(());
147		}
148		waiter.register(&mut self.slot.waiters.borrow_mut());
149		Poll::Pending
150	}
151}
152
153impl Drop for Timer {
154	fn drop(&mut self) {
155		if let Some(key) = self.slot.key.take() {
156			self.heap.borrow_mut().cancel(key);
157		}
158	}
159}
160
161impl std::fmt::Debug for Timer {
162	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163		f.debug_struct("Timer")
164			.field("at", &self.slot.key.get().map(|key| key.0))
165			.field("elapsed", &self.slot.elapsed.get())
166			.finish()
167	}
168}
169
170impl Timer {
171	/// Create a disarmed timer on this worker.
172	pub fn new(handle: &crate::Handle) -> Self {
173		handle.timer()
174	}
175	/// Create a timer that expires after the given duration.
176	pub fn after(handle: &crate::Handle, duration: std::time::Duration) -> Self {
177		let mut timer = handle.timer();
178		timer.set(Instant::now().checked_add(duration));
179		timer
180	}
181	/// Wait for this timer to expire.
182	pub async fn wait(&mut self) {
183		kio::wait(|waiter| self.poll(waiter)).await
184	}
185}