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.
278pub 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            // Terminal work/deadline destruction happens in the caller's poll
306            // frame. Generation cleanup owns only the cancellation signal.
307            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            // Work readiness is provisional until the exact pinned deadline
358            // is checked again. This closes work-poll boundary crossings.
359            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
381/// One complete one-shot timer operation.
382///
383/// Its monotonic deadline is pinned at successful construction. Generation
384/// cancellation wins any uncommitted expiry; natural completion and Drop each
385/// arbitrate the same exact cleanup occurrence, and Drop emits no result.
386pub 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                // Generation cleanup claimed this exact occurrence before
412                // natural completion could disarm it, so cancellation owns
413                // the boundary even if its signal has not executed yet.
414                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}