1use helix_core::Tick;
20use helix_core::TimerId;
21use std::collections::HashMap;
22use std::sync::atomic::{AtomicUsize, Ordering};
23use std::sync::Arc;
24use std::time::Instant;
25use tokio::task::JoinHandle;
26
27use crate::metrics::{
28 AsyncMetricSink, LabelKey, MetricEvent, MetricId, MetricLabels, NoopMetricSink,
29};
30use crate::tick_ingress::EngineTickSender;
31
32pub struct TimerRegistry {
33 handles: HashMap<TimerId, JoinHandle<()>>,
34 done_tx: tokio::sync::mpsc::UnboundedSender<TimerId>,
37 done_rx: tokio::sync::mpsc::UnboundedReceiver<TimerId>,
38 metrics: Arc<dyn AsyncMetricSink>,
39 active: Arc<AtomicUsize>,
40}
41
42impl Default for TimerRegistry {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl TimerRegistry {
49 pub fn new() -> Self {
50 Self::with_metrics(Arc::new(NoopMetricSink))
51 }
52
53 pub(crate) fn with_metrics(metrics: Arc<dyn AsyncMetricSink>) -> Self {
55 let (done_tx, done_rx) = tokio::sync::mpsc::unbounded_channel();
56 let registry = Self {
57 handles: HashMap::new(),
58 done_tx,
59 done_rx,
60 metrics,
61 active: Arc::new(AtomicUsize::new(0)),
62 };
63 record_timer_gauge(registry.metrics.as_ref(), 0);
64 registry
65 }
66
67 fn reap(&mut self) {
75 while let Ok(id) = self.done_rx.try_recv() {
76 self.handles.remove(&id);
77 }
78 }
79
80 pub(crate) fn schedule(&mut self, id: TimerId, after_ms: u64, tick_tx: EngineTickSender) {
87 self.reap();
90 self.cancel_with_reason(id, "replaced");
92
93 let done_tx = self.done_tx.clone();
94 let metrics = Arc::clone(&self.metrics);
95 let active = Arc::clone(&self.active);
96 let due_at = Instant::now() + tokio::time::Duration::from_millis(after_ms);
97 let current = active.fetch_add(1, Ordering::Relaxed) + 1;
98 record_timer_gauge(metrics.as_ref(), current);
99 record_timer_counter(metrics.as_ref(), MetricId::TimerScheduledTotal, "scheduled");
100 let handle = tokio::spawn(async move {
101 tokio::time::sleep(tokio::time::Duration::from_millis(after_ms)).await;
102 record_timer_histogram(
103 metrics.as_ref(),
104 MetricId::TimerLatenessSeconds,
105 Instant::now().saturating_duration_since(due_at),
106 );
107 let delivery_started = Instant::now();
108 let delivered = tick_tx.send(Tick::Timer(id)).await.is_ok();
110 record_timer_histogram(
111 metrics.as_ref(),
112 MetricId::TimerDeliveryWaitSeconds,
113 delivery_started.elapsed(),
114 );
115 if delivered {
116 record_timer_counter(metrics.as_ref(), MetricId::TimerFiredTotal, "delivered");
117 } else {
118 record_timer_counter(
119 metrics.as_ref(),
120 MetricId::TimerDeliveryFailedTotal,
121 "closed",
122 );
123 }
124 let current = decrement_saturating(active.as_ref());
125 record_timer_gauge(metrics.as_ref(), current);
126 done_tx.send(id).ok();
128 });
129 self.handles.insert(id, handle);
130 }
131
132 pub fn cancel(&mut self, id: TimerId) {
134 self.cancel_with_reason(id, "explicit");
135 }
136
137 fn cancel_with_reason(&mut self, id: TimerId, reason: &'static str) {
139 self.reap();
141 if let Some(h) = self.handles.remove(&id) {
142 if h.is_finished() {
143 return;
144 }
145 h.abort();
146 let current = decrement_saturating(self.active.as_ref());
147 record_timer_gauge(self.metrics.as_ref(), current);
148 record_timer_counter(self.metrics.as_ref(), MetricId::TimerCancelledTotal, reason);
149 }
150 }
151
152 #[cfg(test)]
154 pub fn pending_count(&mut self) -> usize {
155 self.reap();
157 self.handles.len()
158 }
159}
160
161impl Drop for TimerRegistry {
165 fn drop(&mut self) {
166 for (_, h) in self.handles.drain() {
167 h.abort();
168 }
169 }
170}
171
172fn record_timer_counter(metrics: &dyn AsyncMetricSink, id: MetricId, status: &'static str) {
174 if metrics.is_enabled() {
175 let _ = metrics.try_record(MetricEvent::counter(
176 id,
177 1.0,
178 MetricLabels::one(LabelKey::Stage, "timer").with(LabelKey::Status, status),
179 ));
180 }
181}
182
183fn record_timer_histogram(metrics: &dyn AsyncMetricSink, id: MetricId, value: std::time::Duration) {
185 if metrics.is_enabled() {
186 let _ = metrics.try_record(MetricEvent::histogram(
187 id,
188 value.as_secs_f64(),
189 MetricLabels::one(LabelKey::Stage, "timer"),
190 ));
191 }
192}
193
194fn record_timer_gauge(metrics: &dyn AsyncMetricSink, active: usize) {
196 if metrics.is_enabled() {
197 let _ = metrics.try_record(MetricEvent::gauge(
198 MetricId::TimerActive,
199 active as f64,
200 MetricLabels::one(LabelKey::Stage, "timer"),
201 ));
202 }
203}
204
205fn decrement_saturating(value: &AtomicUsize) -> usize {
207 value
208 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
209 Some(current.saturating_sub(1))
210 })
211 .unwrap_or_default()
212 .saturating_sub(1)
213}
214
215#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::metrics::RecordOutcome;
221 use helix_core::effect::TimerId;
222 use std::sync::Mutex;
223 use tokio::sync::mpsc;
224
225 #[derive(Default)]
226 struct RecordingMetricSink(Mutex<Vec<MetricEvent>>);
227
228 impl AsyncMetricSink for RecordingMetricSink {
229 fn try_record(&self, event: MetricEvent) -> RecordOutcome {
231 self.0.lock().unwrap().push(event);
232 RecordOutcome::Accepted
233 }
234 }
235
236 #[tokio::test]
237 async fn test_timer_fires_after_delay() {
238 let (tx, mut rx) = mpsc::channel::<Tick>(8);
239 let mut registry = TimerRegistry::new();
240
241 let id = TimerId::from_raw(42);
242 registry.schedule(id, 10, EngineTickSender::Raw(tx)); let tick = tokio::time::timeout(tokio::time::Duration::from_millis(200), rx.recv())
245 .await
246 .expect("timer should fire within 200ms")
247 .expect("channel should not be closed");
248
249 assert!(matches!(tick, Tick::Timer(tid) if tid == id));
250 }
251
252 #[tokio::test]
253 async fn test_cancel_prevents_fire() {
254 let (tx, mut rx) = mpsc::channel::<Tick>(8);
255 let mut registry = TimerRegistry::new();
256
257 let id = TimerId::from_raw(99);
258 registry.schedule(id, 10_000, EngineTickSender::Raw(tx.clone())); registry.cancel(id);
261 let result = tokio::time::timeout(tokio::time::Duration::from_millis(50), rx.recv()).await;
265
266 assert!(
268 result.is_err(),
269 "cancelled timer should not fire within 50ms, result was ready"
270 );
271
272 drop(tx);
274 }
275
276 #[tokio::test]
277 async fn test_reschedule_overwrites() {
278 let (tx, mut rx) = mpsc::channel::<Tick>(8);
279 let mut registry = TimerRegistry::new();
280
281 let id = TimerId::from_raw(7);
282 registry.schedule(id, 10_000, EngineTickSender::Raw(tx.clone())); registry.schedule(id, 10, EngineTickSender::Raw(tx)); let tick = tokio::time::timeout(tokio::time::Duration::from_millis(200), rx.recv())
286 .await
287 .expect("reschedule should fire fast")
288 .expect("channel open");
289
290 assert!(matches!(tick, Tick::Timer(tid) if tid == id));
291 }
292
293 #[tokio::test]
294 async fn test_cancel_idempotent() {
295 let mut registry = TimerRegistry::new();
296 let id = TimerId::from_raw(1);
297 registry.cancel(id);
299 registry.cancel(id);
300 }
301
302 #[tokio::test]
304 async fn test_oneshot_timer_reaped_after_fire() {
305 let (tx, mut rx) = mpsc::channel::<Tick>(8);
306 let mut registry = TimerRegistry::new();
307
308 let id = TimerId::from_raw(123);
309 registry.schedule(id, 10, EngineTickSender::Raw(tx)); let tick = tokio::time::timeout(tokio::time::Duration::from_millis(200), rx.recv())
313 .await
314 .expect("timer should fire within 200ms")
315 .expect("channel should not be closed");
316 assert!(matches!(tick, Tick::Timer(tid) if tid == id));
317
318 tokio::task::yield_now().await;
321
322 assert_eq!(
324 registry.pending_count(),
325 0,
326 "one-shot timer should be reaped from handles after natural fire"
327 );
328 }
329
330 #[tokio::test]
332 async fn observed_timer_records_lifecycle_without_timer_id_label() {
333 let metrics = Arc::new(RecordingMetricSink::default());
334 let (tx, mut rx) = mpsc::channel::<Tick>(8);
335 let mut registry = TimerRegistry::with_metrics(metrics.clone());
336
337 registry.schedule(TimerId::from_raw(321), 1, EngineTickSender::Raw(tx));
338 tokio::time::timeout(tokio::time::Duration::from_millis(200), rx.recv())
339 .await
340 .expect("timer 应在窗口内触发")
341 .expect("timer channel 应保持打开");
342 tokio::task::yield_now().await;
343
344 let events = metrics.0.lock().unwrap();
345 for expected in [
346 MetricId::TimerScheduledTotal,
347 MetricId::TimerFiredTotal,
348 MetricId::TimerLatenessSeconds,
349 MetricId::TimerDeliveryWaitSeconds,
350 MetricId::TimerActive,
351 ] {
352 assert!(
353 events.iter().any(|event| event.id == expected),
354 "timer 缺少 {:?}",
355 expected
356 );
357 }
358 assert!(events
359 .iter()
360 .filter(|event| event.id == MetricId::TimerActive)
361 .any(|event| event.value == 0.0));
362 }
363}