Skip to main content

cordis_timer/
shapes.rs

1//! Complete timer operations bound to the registering Context generation.
2
3use 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
22/// Timer operations bound to a context's current generation.
23pub trait TimerExt: private::Sealed {
24    /// Construct a complete work-owning Timeout.
25    ///
26    /// The deadline is pinned synchronously while `work` remains lazy. The
27    /// operation owns `work`; generation cleanup owns only cancellation.
28    fn timeout<F: Future>(
29        &self,
30        delay: Duration,
31        work: F,
32    ) -> Result<Timeout<F>, TimerRegistrationError>;
33
34    /// Construct one complete, deadline-pinned Sleep.
35    ///
36    /// Registration is synchronous and fallible. The delay may be zero. The
37    /// constructor validates generation admission before the Timer environment,
38    /// pins the monotonic deadline before committing generation cleanup, and
39    /// returns no operation on any failure.
40    fn sleep(&self, delay: Duration) -> Result<Sleep, TimerRegistrationError>;
41
42    /// Construct a fixed-phase Interval anchored at successful construction.
43    ///
44    /// A zero period is rejected before Context admission and Timer environment
45    /// validation. Missed ticks coalesce without shifting the construction-time
46    /// phase, and generation cancellation is one explicit terminal stream item.
47    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    /// Claim natural completion/abandonment against the exact generation
160    /// cleanup occurrence. A false result means generation cleanup already
161    /// owns the occurrence, so no normal one-shot result may commit.
162    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
176/// A construction-anchored fixed-phase timer stream.
177///
178/// On-time ticks yield `Ok(())`. If observation is late, missed ticks coalesce
179/// to at most one overdue item and the next deadline remains on the original
180/// construction-time phase. Generation cancellation yields exactly one
181/// `Err(TimerCancelled)` and then terminates the stream. Dropping the Interval
182/// abandons it without emitting a value.
183pub 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/// The normal terminal outcome of a [`Timeout`].
235#[derive(Debug)]
236pub enum TimeoutOutcome<T> {
237    /// The owned work became ready before the pinned deadline elapsed.
238    Completed(T),
239    /// The pinned deadline elapsed before work readiness could commit.
240    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
271/// One lazy caller Future racing a deadline pinned at construction.
272///
273/// The work Future is never polled by construction or generation cleanup. A
274/// ready work value commits only after the deadline is re-polled and remains
275/// unelapsed; lifecycle cancellation is reported separately from normal expiry.
276/// Dropping abandons the operation, disarms cleanup when possible, and drops
277/// caller work in the caller's frame without emitting a cancellation result.
278/// An arbitrary completed `F::Output` is not retained for replay.
279///
280/// # Panics
281///
282/// Panics if polled again after returning its terminal result.
283pub struct Timeout<F: Future> {
284    work: Option<Pin<Box<F>>>,
285    deadline: Option<Pin<Box<tokio::time::Sleep>>>,
286    cancellation: Arc<GenerationCancellation>,
287    cleanup: EffectEntry,
288    terminated: bool,
289}
290
291impl<F: Future> Future for Timeout<F> {
292    type Output = Result<TimeoutOutcome<F::Output>, TimerCancelled>;
293
294    fn poll(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
295        let this = self.get_mut();
296        assert!(!this.terminated, "polled Timeout after completion");
297        let result = {
298            let work = this.work.as_mut().expect("live Timeout owns work");
299            let deadline = this.deadline.as_mut().expect("live Timeout owns deadline");
300            poll_timeout(
301                work.as_mut(),
302                deadline.as_mut(),
303                &this.cancellation,
304                &mut this.cleanup,
305                cx,
306            )
307        };
308        if result.is_ready() {
309            this.terminated = true;
310            // Terminal work/deadline destruction happens in the caller's poll
311            // frame. Generation cleanup owns only the cancellation signal.
312            this.work.take();
313            this.deadline.take();
314        }
315        result
316    }
317}
318
319fn poll_timeout<F, D>(
320    mut work: Pin<&mut F>,
321    mut deadline: Pin<&mut D>,
322    cancellation: &GenerationCancellation,
323    cleanup: &mut EffectEntry,
324    cx: &mut TaskContext<'_>,
325) -> Poll<Result<TimeoutOutcome<F::Output>, TimerCancelled>>
326where
327    F: Future,
328    D: Future<Output = ()>,
329{
330    if cancellation.is_cancelled(cx) {
331        cleanup.abandon();
332        return Poll::Ready(Err(TimerCancelled));
333    }
334
335    if deadline.as_mut().poll(cx).is_ready() {
336        if cancellation.is_cancelled(cx) {
337            cleanup.abandon();
338            return Poll::Ready(Err(TimerCancelled));
339        }
340        return if cleanup.disarm() {
341            Poll::Ready(Ok(TimeoutOutcome::Elapsed))
342        } else {
343            Poll::Ready(Err(TimerCancelled))
344        };
345    }
346
347    match work.as_mut().poll(cx) {
348        Poll::Pending => {
349            if cancellation.is_cancelled(cx) {
350                cleanup.abandon();
351                Poll::Ready(Err(TimerCancelled))
352            } else {
353                Poll::Pending
354            }
355        }
356        Poll::Ready(output) => {
357            if cancellation.is_cancelled(cx) {
358                cleanup.abandon();
359                return Poll::Ready(Err(TimerCancelled));
360            }
361
362            // Work readiness is provisional until the exact pinned deadline
363            // is checked again. This closes work-poll boundary crossings.
364            if deadline.as_mut().poll(cx).is_ready() {
365                if cancellation.is_cancelled(cx) {
366                    cleanup.abandon();
367                    return Poll::Ready(Err(TimerCancelled));
368                }
369                if cleanup.disarm() {
370                    Poll::Ready(Ok(TimeoutOutcome::Elapsed))
371                } else {
372                    Poll::Ready(Err(TimerCancelled))
373                }
374            } else if cancellation.is_cancelled(cx) {
375                cleanup.abandon();
376                Poll::Ready(Err(TimerCancelled))
377            } else if cleanup.disarm() {
378                Poll::Ready(Ok(TimeoutOutcome::Completed(output)))
379            } else {
380                Poll::Ready(Err(TimerCancelled))
381            }
382        }
383    }
384}
385
386/// One complete one-shot timer operation.
387///
388/// Its monotonic deadline is pinned at successful construction. Generation
389/// cancellation wins any uncommitted expiry; natural completion and Drop each
390/// arbitrate the same exact cleanup occurrence, and Drop emits no result.
391///
392/// # Panics
393///
394/// Panics if polled again after returning its terminal result.
395pub struct Sleep {
396    deadline: Option<Pin<Box<tokio::time::Sleep>>>,
397    cancellation: Arc<GenerationCancellation>,
398    cleanup: EffectEntry,
399    terminated: bool,
400}
401
402impl Future for Sleep {
403    type Output = Result<(), TimerCancelled>;
404
405    fn poll(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
406        let this = self.get_mut();
407        assert!(!this.terminated, "polled Sleep after completion");
408
409        let deadline = this.deadline.as_mut().expect("live Sleep owns deadline");
410        let result = if this.cancellation.is_cancelled(cx) {
411            this.cleanup.abandon();
412            Poll::Ready(Err(TimerCancelled))
413        } else if deadline.as_mut().poll(cx).is_ready() {
414            if this.cancellation.is_cancelled(cx) {
415                this.cleanup.abandon();
416                Poll::Ready(Err(TimerCancelled))
417            } else if this.cleanup.disarm() {
418                Poll::Ready(Ok(()))
419            } else {
420                // Generation cleanup claimed this exact occurrence before
421                // natural completion could disarm it, so cancellation owns
422                // the boundary even if its signal has not executed yet.
423                Poll::Ready(Err(TimerCancelled))
424            }
425        } else if this.cancellation.is_cancelled(cx) {
426            this.cleanup.abandon();
427            Poll::Ready(Err(TimerCancelled))
428        } else {
429            Poll::Pending
430        };
431
432        if result.is_ready() {
433            this.terminated = true;
434            this.deadline.take();
435        }
436        result
437    }
438}
439
440#[cfg(test)]
441mod timeout_arbitration_tests {
442    use super::*;
443    use std::cell::Cell;
444    use std::task::Waker;
445
446    struct BoundaryWork<'a>(&'a Cell<bool>);
447    impl Future for BoundaryWork<'_> {
448        type Output = ();
449        fn poll(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<()> {
450            self.0.set(true);
451            Poll::Ready(())
452        }
453    }
454
455    struct BoundaryDeadline<'a>(&'a Cell<bool>);
456    impl Future for BoundaryDeadline<'_> {
457        type Output = ();
458        fn poll(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<()> {
459            if self.0.get() {
460                Poll::Ready(())
461            } else {
462                Poll::Pending
463            }
464        }
465    }
466
467    #[test]
468    fn work_ready_is_rechecked_against_deadline_before_commit() {
469        let boundary = Cell::new(false);
470        let mut work = BoundaryWork(&boundary);
471        let mut deadline = BoundaryDeadline(&boundary);
472        let cancellation = GenerationCancellation::new();
473        let ctx = Context::new();
474        let mut cleanup = EffectEntry(Some(ctx.effect_sync(|| {}).unwrap()));
475        let waker = Waker::noop();
476        let mut cx = TaskContext::from_waker(waker);
477
478        let result = poll_timeout(
479            Pin::new(&mut work),
480            Pin::new(&mut deadline),
481            &cancellation,
482            &mut cleanup,
483            &mut cx,
484        );
485        assert!(matches!(result, Poll::Ready(Ok(TimeoutOutcome::Elapsed))));
486    }
487}