1use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::task::{Context as TaskContext, Poll};
8use std::time::Duration;
9
10use cordis_core::Context;
11use cordis_core::effect::EffectRegistration;
12use futures::Stream;
13use futures::task::AtomicWaker;
14
15use crate::{TimerCancelled, TimerRegistrationError};
16
17mod private {
18 pub trait Sealed {}
19 impl Sealed for cordis_core::Context {}
20}
21
22pub trait TimerExt: private::Sealed {
24 fn timeout<F: Future>(
29 &self,
30 delay: Duration,
31 work: F,
32 ) -> Result<Timeout<F>, TimerRegistrationError>;
33
34 fn sleep(&self, delay: Duration) -> Result<Sleep, TimerRegistrationError>;
41
42 fn interval(&self, period: Duration) -> Result<Interval, TimerRegistrationError>;
48}
49
50fn prepare_deadline(
51 delay: Duration,
52) -> Result<Pin<Box<tokio::time::Sleep>>, TimerRegistrationError> {
53 if tokio::runtime::Handle::try_current().is_err() {
54 return Err(TimerRegistrationError::TimerUnavailable);
55 }
56 let now = tokio::time::Instant::now();
57 let deadline = now
58 .checked_add(delay)
59 .ok_or(TimerRegistrationError::DeadlineOutOfRange)?;
60 std::panic::catch_unwind(|| Box::pin(tokio::time::sleep_until(deadline)))
61 .map_err(|_| TimerRegistrationError::TimerUnavailable)
62}
63
64fn prepare_interval(period: Duration) -> Result<tokio::time::Interval, TimerRegistrationError> {
65 if tokio::runtime::Handle::try_current().is_err() {
66 return Err(TimerRegistrationError::TimerUnavailable);
67 }
68 let now = tokio::time::Instant::now();
69 let first = now
70 .checked_add(period)
71 .ok_or(TimerRegistrationError::DeadlineOutOfRange)?;
72 std::panic::catch_unwind(|| {
73 let mut interval = tokio::time::interval_at(first, period);
74 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
75 interval
76 })
77 .map_err(|_| TimerRegistrationError::TimerUnavailable)
78}
79
80fn commit_cancellation(
81 ctx: &Context,
82) -> Result<(Arc<GenerationCancellation>, EffectEntry), TimerRegistrationError> {
83 let cancellation = Arc::new(GenerationCancellation::new());
84 let cleanup_signal = Arc::clone(&cancellation);
85 let registration = ctx
86 .effect_sync(move || cleanup_signal.cancel())
87 .map_err(|_| TimerRegistrationError::InactiveContext)?;
88 Ok((cancellation, EffectEntry(Some(registration))))
89}
90
91fn prepare_one_shot(
92 ctx: &Context,
93 delay: Duration,
94) -> Result<OneShotParts, TimerRegistrationError> {
95 if !cordis_core::__internal::generation_cleanup_admitted(ctx) {
96 return Err(TimerRegistrationError::InactiveContext);
97 }
98 let deadline = prepare_deadline(delay)?;
99 let (cancellation, cleanup) = commit_cancellation(ctx)?;
100 Ok((deadline, cancellation, cleanup))
101}
102
103type OneShotParts = (
104 Pin<Box<tokio::time::Sleep>>,
105 Arc<GenerationCancellation>,
106 EffectEntry,
107);
108
109impl TimerExt for Context {
110 fn timeout<F: Future>(
111 &self,
112 delay: Duration,
113 work: F,
114 ) -> Result<Timeout<F>, TimerRegistrationError> {
115 if !cordis_core::__internal::generation_cleanup_admitted(self) {
116 return Err(TimerRegistrationError::InactiveContext);
117 }
118 let deadline = prepare_deadline(delay)?;
119 let (cancellation, cleanup) = commit_cancellation(self)?;
120 Ok(Timeout {
121 work: Some(Box::pin(work)),
122 deadline: Some(deadline),
123 cancellation,
124 cleanup,
125 terminated: false,
126 })
127 }
128
129 fn sleep(&self, delay: Duration) -> Result<Sleep, TimerRegistrationError> {
130 let (deadline, cancellation, cleanup) = prepare_one_shot(self, delay)?;
131 Ok(Sleep {
132 deadline: Some(deadline),
133 cancellation,
134 cleanup,
135 terminated: false,
136 })
137 }
138
139 fn interval(&self, period: Duration) -> Result<Interval, TimerRegistrationError> {
140 if period.is_zero() {
141 return Err(TimerRegistrationError::ZeroPeriod);
142 }
143 if !cordis_core::__internal::generation_cleanup_admitted(self) {
144 return Err(TimerRegistrationError::InactiveContext);
145 }
146 let scheduler = prepare_interval(period)?;
147 let (cancellation, cleanup) = commit_cancellation(self)?;
148 Ok(Interval {
149 scheduler: Some(scheduler),
150 cancellation,
151 cleanup,
152 terminated: false,
153 })
154 }
155}
156
157struct EffectEntry(Option<EffectRegistration>);
158impl EffectEntry {
159 fn disarm(&mut self) -> bool {
163 self.0.take().is_some_and(EffectRegistration::disarm)
164 }
165
166 fn abandon(&mut self) {
167 let _ = self.disarm();
168 }
169}
170impl Drop for EffectEntry {
171 fn drop(&mut self) {
172 self.abandon();
173 }
174}
175
176pub struct Interval {
184 scheduler: Option<tokio::time::Interval>,
185 cancellation: Arc<GenerationCancellation>,
186 cleanup: EffectEntry,
187 terminated: bool,
188}
189
190impl Stream for Interval {
191 type Item = Result<(), TimerCancelled>;
192
193 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
194 let this = self.get_mut();
195 if this.terminated {
196 return Poll::Ready(None);
197 }
198 if this.cancellation.is_cancelled(cx) {
199 this.terminated = true;
200 this.cleanup.abandon();
201 this.scheduler.take();
202 return Poll::Ready(Some(Err(TimerCancelled)));
203 }
204
205 let scheduler = this
206 .scheduler
207 .as_mut()
208 .expect("live Interval has scheduler");
209 match Pin::new(scheduler).poll_tick(cx) {
210 Poll::Pending => {
211 if this.cancellation.is_cancelled(cx) {
212 this.terminated = true;
213 this.cleanup.abandon();
214 this.scheduler.take();
215 Poll::Ready(Some(Err(TimerCancelled)))
216 } else {
217 Poll::Pending
218 }
219 }
220 Poll::Ready(_) => {
221 if this.cancellation.is_cancelled(cx) {
222 this.terminated = true;
223 this.cleanup.abandon();
224 this.scheduler.take();
225 Poll::Ready(Some(Err(TimerCancelled)))
226 } else {
227 Poll::Ready(Some(Ok(())))
228 }
229 }
230 }
231 }
232}
233
234#[derive(Debug)]
236pub enum TimeoutOutcome<T> {
237 Completed(T),
239 Elapsed,
241}
242
243struct GenerationCancellation {
244 cancelled: AtomicBool,
245 waker: AtomicWaker,
246}
247
248impl GenerationCancellation {
249 fn new() -> Self {
250 Self {
251 cancelled: AtomicBool::new(false),
252 waker: AtomicWaker::new(),
253 }
254 }
255
256 fn cancel(&self) {
257 if !self.cancelled.swap(true, Ordering::AcqRel) {
258 self.waker.wake();
259 }
260 }
261
262 fn is_cancelled(&self, cx: &TaskContext<'_>) -> bool {
263 if self.cancelled.load(Ordering::Acquire) {
264 return true;
265 }
266 self.waker.register(cx.waker());
267 self.cancelled.load(Ordering::Acquire)
268 }
269}
270
271pub struct Timeout<F: Future> {
279 work: Option<Pin<Box<F>>>,
280 deadline: Option<Pin<Box<tokio::time::Sleep>>>,
281 cancellation: Arc<GenerationCancellation>,
282 cleanup: EffectEntry,
283 terminated: bool,
284}
285
286impl<F: Future> Future for Timeout<F> {
287 type Output = Result<TimeoutOutcome<F::Output>, TimerCancelled>;
288
289 fn poll(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
290 let this = self.get_mut();
291 assert!(!this.terminated, "polled Timeout after completion");
292 let result = {
293 let work = this.work.as_mut().expect("live Timeout owns work");
294 let deadline = this.deadline.as_mut().expect("live Timeout owns deadline");
295 poll_timeout(
296 work.as_mut(),
297 deadline.as_mut(),
298 &this.cancellation,
299 &mut this.cleanup,
300 cx,
301 )
302 };
303 if result.is_ready() {
304 this.terminated = true;
305 this.work.take();
308 this.deadline.take();
309 }
310 result
311 }
312}
313
314fn poll_timeout<F, D>(
315 mut work: Pin<&mut F>,
316 mut deadline: Pin<&mut D>,
317 cancellation: &GenerationCancellation,
318 cleanup: &mut EffectEntry,
319 cx: &mut TaskContext<'_>,
320) -> Poll<Result<TimeoutOutcome<F::Output>, TimerCancelled>>
321where
322 F: Future,
323 D: Future<Output = ()>,
324{
325 if cancellation.is_cancelled(cx) {
326 cleanup.abandon();
327 return Poll::Ready(Err(TimerCancelled));
328 }
329
330 if deadline.as_mut().poll(cx).is_ready() {
331 if cancellation.is_cancelled(cx) {
332 cleanup.abandon();
333 return Poll::Ready(Err(TimerCancelled));
334 }
335 return if cleanup.disarm() {
336 Poll::Ready(Ok(TimeoutOutcome::Elapsed))
337 } else {
338 Poll::Ready(Err(TimerCancelled))
339 };
340 }
341
342 match work.as_mut().poll(cx) {
343 Poll::Pending => {
344 if cancellation.is_cancelled(cx) {
345 cleanup.abandon();
346 Poll::Ready(Err(TimerCancelled))
347 } else {
348 Poll::Pending
349 }
350 }
351 Poll::Ready(output) => {
352 if cancellation.is_cancelled(cx) {
353 cleanup.abandon();
354 return Poll::Ready(Err(TimerCancelled));
355 }
356
357 if deadline.as_mut().poll(cx).is_ready() {
360 if cancellation.is_cancelled(cx) {
361 cleanup.abandon();
362 return Poll::Ready(Err(TimerCancelled));
363 }
364 if cleanup.disarm() {
365 Poll::Ready(Ok(TimeoutOutcome::Elapsed))
366 } else {
367 Poll::Ready(Err(TimerCancelled))
368 }
369 } else if cancellation.is_cancelled(cx) {
370 cleanup.abandon();
371 Poll::Ready(Err(TimerCancelled))
372 } else if cleanup.disarm() {
373 Poll::Ready(Ok(TimeoutOutcome::Completed(output)))
374 } else {
375 Poll::Ready(Err(TimerCancelled))
376 }
377 }
378 }
379}
380
381pub struct Sleep {
387 deadline: Option<Pin<Box<tokio::time::Sleep>>>,
388 cancellation: Arc<GenerationCancellation>,
389 cleanup: EffectEntry,
390 terminated: bool,
391}
392
393impl Future for Sleep {
394 type Output = Result<(), TimerCancelled>;
395
396 fn poll(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
397 let this = self.get_mut();
398 assert!(!this.terminated, "polled Sleep after completion");
399
400 let deadline = this.deadline.as_mut().expect("live Sleep owns deadline");
401 let result = if this.cancellation.is_cancelled(cx) {
402 this.cleanup.abandon();
403 Poll::Ready(Err(TimerCancelled))
404 } else if deadline.as_mut().poll(cx).is_ready() {
405 if this.cancellation.is_cancelled(cx) {
406 this.cleanup.abandon();
407 Poll::Ready(Err(TimerCancelled))
408 } else if this.cleanup.disarm() {
409 Poll::Ready(Ok(()))
410 } else {
411 Poll::Ready(Err(TimerCancelled))
415 }
416 } else if this.cancellation.is_cancelled(cx) {
417 this.cleanup.abandon();
418 Poll::Ready(Err(TimerCancelled))
419 } else {
420 Poll::Pending
421 };
422
423 if result.is_ready() {
424 this.terminated = true;
425 this.deadline.take();
426 }
427 result
428 }
429}
430
431#[cfg(test)]
432mod timeout_arbitration_tests {
433 use super::*;
434 use std::cell::Cell;
435 use std::task::Waker;
436
437 struct BoundaryWork<'a>(&'a Cell<bool>);
438 impl Future for BoundaryWork<'_> {
439 type Output = ();
440 fn poll(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<()> {
441 self.0.set(true);
442 Poll::Ready(())
443 }
444 }
445
446 struct BoundaryDeadline<'a>(&'a Cell<bool>);
447 impl Future for BoundaryDeadline<'_> {
448 type Output = ();
449 fn poll(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<()> {
450 if self.0.get() {
451 Poll::Ready(())
452 } else {
453 Poll::Pending
454 }
455 }
456 }
457
458 #[test]
459 fn work_ready_is_rechecked_against_deadline_before_commit() {
460 let boundary = Cell::new(false);
461 let mut work = BoundaryWork(&boundary);
462 let mut deadline = BoundaryDeadline(&boundary);
463 let cancellation = GenerationCancellation::new();
464 let ctx = Context::new();
465 let mut cleanup = EffectEntry(Some(ctx.effect_sync(|| {}).unwrap()));
466 let waker = Waker::noop();
467 let mut cx = TaskContext::from_waker(waker);
468
469 let result = poll_timeout(
470 Pin::new(&mut work),
471 Pin::new(&mut deadline),
472 &cancellation,
473 &mut cleanup,
474 &mut cx,
475 );
476 assert!(matches!(result, Poll::Ready(Ok(TimeoutOutcome::Elapsed))));
477 }
478}