Skip to main content

windows_threadpool_sys/timer/
periodic.rs

1// Copyright (c) 2026 Mike Grier
2//! Periodic thread-pool timers.
3
4use std::io;
5use std::ptr;
6use std::sync::atomic::{AtomicIsize, Ordering};
7use std::time::{Duration, SystemTime};
8
9use windows_sys::Win32::Foundation::FALSE;
10use windows_sys::Win32::System::Threading::{
11    CloseThreadpoolTimer, CreateThreadpoolTimer, IsThreadpoolTimerSet, PTP_CALLBACK_INSTANCE,
12    PTP_TIMER, WaitForThreadpoolTimerCallbacks,
13};
14
15use crate::callback_env::CallbackEnviron;
16use crate::timer::{absolute_filetime, arm_raw, disarm_raw, millis_u32, relative_filetime};
17
18/// Heap-allocated callback state kept alive for the lifetime of the timer.
19///
20/// `timer` is filled in after `CreateThreadpoolTimer` returns, because stopping
21/// from inside a callback needs the object the callback belongs to.
22struct PeriodicContext {
23    timer: AtomicIsize,
24    /// Whether the timer has a schedule that nothing has drained.
25    ///
26    /// A tick does **not** settle this, which is where this type parts company
27    /// with the one-shot timer and the wait. Those are armed for exactly one
28    /// activation, so a dispatch leaves nothing behind; here the pool re-arms
29    /// from the period, so the timer is just as live after a tick as before it.
30    /// Only stopping it settles this.
31    obligation: crate::obligation::CloseObligation,
32    /// This pool's entry in the self-heal registry.
33    ///
34    /// Held for its `Drop`, not read. The claim keeps the entry alive while
35    /// this object exists, which is what makes the entry's pre-created repair
36    /// work object available -- and bound to the pool, deferring its free --
37    /// if a wait on the same pool is later cancelled. Only a wait reads a
38    /// registration, because only a wait reaches the removal primitive that
39    /// owes a repair.
40    #[allow(dead_code)]
41    registration: crate::heal::Registration,
42    callback: Box<dyn Fn(&PeriodicTick<'_>) + Send + Sync + 'static>,
43}
44
45/// One tick of a [`ThreadpoolPeriodicTimer`], handed to its callback.
46///
47/// A tick may be running concurrently with other ticks of the same timer, so
48/// anything this callback touches must tolerate that.
49pub struct PeriodicTick<'ctx> {
50    ctx: &'ctx PeriodicContext,
51}
52
53impl std::fmt::Debug for PeriodicTick<'_> {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        f.debug_struct("PeriodicTick").finish_non_exhaustive()
56    }
57}
58
59impl PeriodicTick<'_> {
60    /// Stop the timer from inside its own callback.
61    ///
62    /// This is how a periodic timer ends itself -- "tick until some condition
63    /// holds" needs no external coordination.
64    ///
65    /// It stops *future* ticks being queued, and discards a tick already queued
66    /// -- the behaviour
67    /// `stopping_a_periodic_timer_discards_a_tick_that_is_already_queued`
68    /// measures for [`ThreadpoolPeriodicTimer::stop`], which this calls. What
69    /// it does not affect is a tick already *running*, including another
70    /// concurrent run of this same callback on another thread. Expect the
71    /// callback to run again after calling this, and make it idempotent
72    /// accordingly.
73    pub fn stop(&self) {
74        let timer = self.ctx.timer.load(Ordering::Acquire);
75        debug_assert_ne!(timer, 0, "the timer must be published before callbacks");
76        // SAFETY: `timer` is this object's live PTP_TIMER, published before any
77        // callback could run.
78        unsafe { disarm_raw(timer) };
79    }
80}
81
82/// Trampoline from the raw `PTP_TIMER_CALLBACK` ABI into the boxed closure.
83///
84/// SAFETY: `context` must point to a live [`PeriodicContext`] for the entire
85/// duration of every callback invocation, which [`ThreadpoolPeriodicTimer`]'s `Drop`
86/// ordering guarantees.
87unsafe extern "system" fn periodic_trampoline(
88    _instance: PTP_CALLBACK_INSTANCE,
89    context: *mut core::ffi::c_void,
90    _timer: PTP_TIMER,
91) {
92    // SAFETY: context is a valid *mut PeriodicContext for the full callback duration.
93    let ctx = unsafe { &*(context as *const PeriodicContext) };
94    // The target begins with `timer` so that narrowing to `timer` catches both
95    // kinds, and continues so that narrowing to `timer-periodic` catches only
96    // this one.
97    crate::trace_record!("timer-periodic", "trampoline-entered", _timer);
98    // Stamped before the callback, not after: a dispatch that is still running
99    // is evidence the pool is live, and a long callback must not look like
100    // silence to the self-heal.
101    let tick = PeriodicTick { ctx };
102    // Not contained: see the callback contract in the crate docs.
103    (ctx.callback)(&tick);
104    crate::trace_record!("timer-periodic", "trampoline-left", _timer);
105}
106
107/// An owned repeating thread-pool timer.
108///
109/// The period is fixed when the timer is created, so the type says what it is:
110/// this object exists to tick on a cadence, and there is no argument that can
111/// quietly turn it into a one-shot.
112///
113/// # Ticks can overlap
114///
115/// **The pool queues each tick on schedule regardless of whether the previous
116/// tick has finished.** If the callback takes longer than the period, two or
117/// more runs of it will execute concurrently on different pool threads. This is
118/// the property that makes periodic timers surprising in practice, and it
119/// follows from the cadence being fixed: the schedule cannot wait for the
120/// callback without ceasing to be a schedule.
121///
122/// So a `ThreadpoolPeriodicTimer` callback must be safe to run concurrently with itself.
123/// If that is awkward, the alternative is a one-shot [`ThreadpoolTimer`](crate::timer::ThreadpoolTimer) re-armed from
124/// inside its own callback with [`crate::timer::TimerFiring::rearm_after`]:
125/// there is never more than one firing outstanding, and the gap is measured from
126/// the end of each firing rather than from a fixed schedule.
127///
128/// |  | [`ThreadpoolPeriodicTimer`] | [`ThreadpoolTimer`](crate::timer::ThreadpoolTimer) + `rearm_after` |
129/// |---|---|---|
130/// | Cadence | fixed, independent of callback duration | drifts by the callback duration |
131/// | Concurrent runs of the callback | possible | never |
132/// | Slow callback | ticks pile up and overlap | next tick simply happens later |
133///
134/// # Teardown
135///
136/// [`Drop`] stops the timer before draining callbacks, so it cannot requeue
137/// during teardown. [`ThreadpoolPeriodicTimer::stop_and_drain`] does the same thing under
138/// the caller's control, and is the ordering to copy if doing it by hand:
139/// stop first, drain second.
140///
141/// # Examples
142///
143/// ```
144/// use std::sync::Arc;
145/// use std::sync::atomic::{AtomicUsize, Ordering};
146/// use std::time::Duration;
147/// use windows_threadpool_sys::timer::ThreadpoolPeriodicTimer;
148///
149/// let ticks = Arc::new(AtomicUsize::new(0));
150/// let counter = Arc::clone(&ticks);
151///
152/// // The period belongs to the timer, not to a call.
153/// let timer = ThreadpoolPeriodicTimer::new(Duration::from_millis(1), move |_tick| {
154///     counter.fetch_add(1, Ordering::SeqCst);
155/// }, None)?;
156///
157/// timer.start();
158/// while ticks.load(Ordering::SeqCst) < 3 {
159///     std::thread::yield_now();
160/// }
161///
162/// timer.stop_and_drain();
163/// assert!(ticks.load(Ordering::SeqCst) >= 3);
164/// # Ok::<(), std::io::Error>(())
165/// ```
166///
167/// Stopping from inside the callback, for "tick until done". Note the counter
168/// may pass the threshold: the stop discards a queued tick, but a tick already
169/// running on another thread is unaffected and will finish its increment:
170///
171/// ```
172/// use std::sync::Arc;
173/// use std::sync::atomic::{AtomicUsize, Ordering};
174/// use std::time::Duration;
175/// use windows_threadpool_sys::timer::ThreadpoolPeriodicTimer;
176///
177/// let ticks = Arc::new(AtomicUsize::new(0));
178/// let counter = Arc::clone(&ticks);
179///
180/// let timer = ThreadpoolPeriodicTimer::new(Duration::from_millis(1), move |tick| {
181///     if counter.fetch_add(1, Ordering::SeqCst) >= 2 {
182///         tick.stop();
183///     }
184/// }, None)?;
185///
186/// timer.start();
187/// while timer.is_running() {
188///     std::thread::yield_now();
189/// }
190/// timer.stop_and_drain();
191/// assert!(ticks.load(Ordering::SeqCst) >= 3);
192/// # Ok::<(), std::io::Error>(())
193/// ```
194pub struct ThreadpoolPeriodicTimer {
195    timer: PTP_TIMER,
196    period: Duration,
197    // Kept alive as a raw pointer until Drop has stopped and drained.
198    context: *mut PeriodicContext,
199}
200
201// SAFETY: PTP_TIMER is a cross-thread pool object, and the context holds a
202// callback that is Fn + Send + Sync; the pointer is only read until Drop frees
203// it after all callbacks have finished.
204unsafe impl Send for ThreadpoolPeriodicTimer {}
205unsafe impl Sync for ThreadpoolPeriodicTimer {}
206
207/// Nanoseconds in a millisecond, for checking a period divides evenly.
208const NANOS_PER_MILLI: u32 = 1_000_000;
209
210impl ThreadpoolPeriodicTimer {
211    /// The shortest period this timer can express: one millisecond.
212    ///
213    /// `SetThreadpoolTimer` takes the period as whole milliseconds, so nothing
214    /// shorter can be represented. This is a floor on what may be *asked for*,
215    /// not a promise about delivery: ticks arrive on the system timer tick,
216    /// which is far coarser (~15.6ms by default).
217    pub const MIN_PERIOD: Duration = Duration::from_millis(1);
218
219    /// The longest period this timer can express: `u32::MAX` milliseconds, or
220    /// just under 50 days.
221    ///
222    /// The pool's period field is a `u32` count of milliseconds, so a longer
223    /// period cannot be represented.
224    pub const MAX_PERIOD: Duration = Duration::from_millis(u32::MAX as u64);
225
226    /// Create a stopped timer that invokes `callback` every `period`.
227    ///
228    /// `period` must be a whole number of milliseconds between
229    /// [`MIN_PERIOD`](Self::MIN_PERIOD) and [`MAX_PERIOD`](Self::MAX_PERIOD);
230    /// anything else is rejected. The pool takes the period as a `u32` count of
231    /// milliseconds, so every other value would be quietly altered to fit:
232    ///
233    /// - a period below a millisecond rounds down to zero, and a zero period
234    ///   means "do not repeat" -- the timer would fire once and stop, which is a
235    ///   `ThreadpoolPeriodicTimer` silently behaving like a
236    ///   [`ThreadpoolTimer`](crate::timer::ThreadpoolTimer);
237    /// - a fractional period such as 1.5ms is truncated, so the timer would tick
238    ///   at 1ms while [`period`](Self::period) still reported 1.5ms;
239    /// - a period beyond `u32::MAX` milliseconds would be capped, ticking far
240    ///   more often than asked.
241    ///
242    /// Rejecting rather than rounding keeps [`period`](Self::period) an accurate
243    /// report of what was scheduled.
244    ///
245    /// Pass `Some(env)` to select a private pool or callback priority; `None`
246    /// uses the process-default pool with default priority.
247    ///
248    /// The callback runs on a shared, process-managed pool thread, may run
249    /// concurrently with itself (see the type documentation), must restore any
250    /// thread state it changes, and must not terminate its thread. It must not
251    /// panic: a panic unwinds to the `extern "system"` trampoline and aborts the
252    /// process.
253    ///
254    /// # Errors
255    ///
256    /// Returns [`io::ErrorKind::InvalidInput`] if `period` is outside
257    /// [`MIN_PERIOD`](Self::MIN_PERIOD)..=[`MAX_PERIOD`](Self::MAX_PERIOD) or is
258    /// not a whole number of milliseconds, or the error from
259    /// `CreateThreadpoolTimer`.
260    pub fn new<F>(
261        period: Duration,
262        callback: F,
263        env: Option<&mut CallbackEnviron>,
264    ) -> io::Result<Self>
265    where
266        F: Fn(&PeriodicTick<'_>) + Send + Sync + 'static,
267    {
268        if period < Self::MIN_PERIOD {
269            return Err(io::Error::new(
270                io::ErrorKind::InvalidInput,
271                "a ThreadpoolPeriodicTimer needs a period of at least 1ms: the pool takes the period in whole milliseconds, so anything shorter rounds to zero and a zero period means do not repeat; use ThreadpoolTimer for a one-shot",
272            ));
273        }
274        if period > Self::MAX_PERIOD {
275            return Err(io::Error::new(
276                io::ErrorKind::InvalidInput,
277                "a ThreadpoolPeriodicTimer period must fit in u32 milliseconds (just under 50 days); a longer one would be capped and tick far more often than asked",
278            ));
279        }
280        if !period.subsec_nanos().is_multiple_of(NANOS_PER_MILLI) {
281            return Err(io::Error::new(
282                io::ErrorKind::InvalidInput,
283                "a ThreadpoolPeriodicTimer period must be a whole number of milliseconds: the pool truncates the remainder, so the timer would tick sooner than the period it reports",
284            ));
285        }
286
287        // Read before `env` is consumed below, and registered before the object
288        // exists: the entry must be able to repair this pool from the moment
289        // anything of ours can dispatch on it.
290        let registration = crate::heal::register(crate::heal::key_of(env.as_deref()));
291        let context = Box::into_raw(Box::new(PeriodicContext {
292            timer: AtomicIsize::new(0),
293            obligation: crate::obligation::CloseObligation::new(),
294            registration,
295            callback: Box::new(callback),
296        }));
297        let env_ptr = env.map_or(ptr::null_mut(), |e| e.as_mut_ptr());
298
299        let timer = crate::trace_call!("CreateThreadpoolTimer", 0, millis_u32(period), {
300            // SAFETY: context is a valid heap pointer that outlives every callback,
301            // and env_ptr is valid (or null) for the duration of this call.
302            unsafe {
303                CreateThreadpoolTimer(
304                    Some(periodic_trampoline),
305                    context.cast(),
306                    env_ptr.cast_const(),
307                )
308            }
309        });
310
311        if timer == 0 {
312            let error = io::Error::last_os_error();
313            // SAFETY: the pool never saw context; reclaim it immediately.
314            unsafe { drop(Box::from_raw(context)) };
315            return Err(error);
316        }
317
318        // Publish the object before any callback can run. The timer is not
319        // started yet, so no callback can observe the unpublished value.
320        // SAFETY: context is live and exclusively ours until the first start.
321        unsafe { (*context).timer.store(timer, Ordering::Release) };
322        crate::trace_record!("timer-periodic", "created", timer, millis_u32(period));
323
324        Ok(Self {
325            timer,
326            period,
327            context,
328        })
329    }
330
331    /// The period this timer ticks on.
332    #[must_use]
333    pub fn period(&self) -> Duration {
334        self.period
335    }
336
337    /// Start ticking, with the first tick one period from now.
338    pub fn start(&self) {
339        self.start_after(self.period);
340    }
341
342    /// Start ticking, with the first tick `first_delay` from now.
343    ///
344    /// Subsequent ticks follow every [`ThreadpoolPeriodicTimer::period`]. A zero
345    /// `first_delay` makes the first tick due immediately.
346    pub fn start_after(&self, first_delay: Duration) {
347        self.record_live_before(|| {
348            // SAFETY: timer is valid for the lifetime of self.
349            unsafe {
350                arm_raw(
351                    self.timer,
352                    relative_filetime(first_delay),
353                    millis_u32(self.period),
354                    0,
355                );
356            }
357        });
358    }
359
360    /// Start ticking, with the first tick at the wall-clock instant `when`.
361    ///
362    /// Unlike a relative first delay, an absolute one passes through sleep and
363    /// hibernation.
364    pub fn start_at(&self, when: SystemTime) {
365        self.record_live_before(|| {
366            // SAFETY: timer is valid for the lifetime of self.
367            unsafe {
368                arm_raw(
369                    self.timer,
370                    absolute_filetime(when),
371                    millis_u32(self.period),
372                    0,
373                );
374            }
375        });
376    }
377
378    /// Start ticking, allowing the system a coalescing `window` on each tick.
379    ///
380    /// `window` is the tolerance the system may add so it can group this timer
381    /// with other expirations and wake the processor less often, trading timing
382    /// precision for power.
383    pub fn start_with_window(&self, first_delay: Duration, window: Duration) {
384        self.record_live_before(|| {
385            // SAFETY: timer is valid for the lifetime of self.
386            unsafe {
387                arm_raw(
388                    self.timer,
389                    relative_filetime(first_delay),
390                    millis_u32(self.period),
391                    millis_u32(window),
392                );
393            }
394        });
395    }
396
397    /// Stop the timer.
398    ///
399    /// Future ticks stop being queued, and a tick already queued is discarded
400    /// -- measured on this type by
401    /// `stopping_a_periodic_timer_discards_a_tick_that_is_already_queued`,
402    /// which holds the pool's only thread busy so a tick is provably queued,
403    /// then stops. Ticks already *executing* are unaffected; use
404    /// [`ThreadpoolPeriodicTimer::stop_and_drain`] to wait for those.
405    pub fn stop(&self) {
406        // SAFETY: timer is valid for the lifetime of self.
407        unsafe { disarm_raw(self.timer) };
408    }
409
410    /// Whether the timer is currently started.
411    ///
412    /// Ticking does not clear the schedule, so this stays `true` until something
413    /// stops the timer -- [`ThreadpoolPeriodicTimer::stop`], [`PeriodicTick::stop`], or
414    /// teardown.
415    #[must_use]
416    pub fn is_running(&self) -> bool {
417        // SAFETY: timer is valid for the lifetime of self.
418        crate::trace_call!("IsThreadpoolTimerSet", self.timer, 0, {
419            // SAFETY: timer is valid for the lifetime of self.
420            unsafe { IsThreadpoolTimerSet(self.timer) != 0 }
421        })
422    }
423
424    /// Block until all queued and executing ticks have completed.
425    ///
426    /// Stop the timer first, or this waits for a schedule that keeps producing
427    /// new ticks. [`ThreadpoolPeriodicTimer::stop_and_drain`] does both in the right order.
428    pub fn wait(&self) {
429        // SAFETY: timer is valid for the lifetime of self.
430        crate::trace_call!("WaitForThreadpoolTimerCallbacks", self.timer, 0, {
431            // SAFETY: timer is valid for the lifetime of self.
432            unsafe { WaitForThreadpoolTimerCallbacks(self.timer, FALSE) };
433        });
434    }
435
436    /// Stop the timer and wait until no tick is queued or executing.
437    ///
438    /// This is the correct teardown order -- stop first, drain second -- and is
439    /// what [`Drop`] performs.
440    ///
441    /// The drain no longer cancels: it waits for a queued tick to run. On this
442    /// type that is **unobservable**, because `stop` discards a queued tick --
443    /// measured by
444    /// `stopping_a_periodic_timer_discards_a_tick_that_is_already_queued`. That
445    /// test exists because this paragraph used to cite the *one-shot* timer's
446    /// `disarming_cancels_a_queued_tick_which_a_waits_disarm_does_not`
447    /// instead, which measures `SetThreadpoolTimer` with no period at all. The
448    /// drain is the form the rest of the crate uses, and the one that stays
449    /// correct if the discard behaviour ever changes.
450    ///
451    /// The result holds provided no other thread starts the timer during the
452    /// call. `ThreadpoolPeriodicTimer` is `Sync` and the `start*` methods take
453    /// `&self`, so a start landing between the stop and the drain is not
454    /// excluded by anything here, and would leave a schedule installed on
455    /// return. A caller needing the timer to be provably stopped must own it
456    /// exclusively or serialize access to it. Unlike the one-shot timer there is
457    /// no re-arm to suppress: [`PeriodicTick::stop`] only ever stops.
458    pub fn stop_and_drain(&self) {
459        // SAFETY: the context outlives every tick and is freed only by Drop,
460        // which cannot run while this borrow of self is alive.
461        unsafe { Self::stop_and_drain_parts(self.context.cast(), self.timer) };
462    }
463
464    /// [`stop_and_drain`](Self::stop_and_drain) against a detached context.
465    ///
466    /// One body rather than two, for the reason the one-shot timer's twin of
467    /// this function gives -- and this type is the evidence that the reason is
468    /// real rather than tidiness. It had no shared body, and the two copies
469    /// drifted: the drain here was converted from cancelling to waiting while
470    /// the cleanup-group member went on passing `TRUE`, kept its `(cancel)`
471    /// trace label, and never settled the obligation at all, all while its own
472    /// documentation said it did "as with `ThreadpoolPeriodicTimer`".
473    ///
474    /// # Safety
475    ///
476    /// `context` must come from [`into_parts`](Self::into_parts) on this type
477    /// and name a still-live object whose context the caller has not yet freed,
478    /// and `timer` must be that object.
479    pub(crate) unsafe fn stop_and_drain_parts(context: *mut core::ffi::c_void, timer: PTP_TIMER) {
480        // SAFETY: forwarded from this function's own contract.
481        let ctx = unsafe { &*context.cast::<PeriodicContext>() };
482        // SAFETY: `timer` is live for this call by the same contract.
483        unsafe { disarm_raw(timer) };
484        // The disarm above is what makes this terminate: with no period left to
485        // re-queue from, the drain has a finite backlog to run out.
486        crate::trace_call!("WaitForThreadpoolTimerCallbacks", timer, 0, {
487            // SAFETY: `timer` is live, and waiting without cancelling cannot
488            // orphan any storage.
489            unsafe { WaitForThreadpoolTimerCallbacks(timer, FALSE) };
490        });
491        // Settled after the drain. `PeriodicTick::stop` only ever stops, so
492        // unlike the one-shot timer there is no re-arm that could have landed
493        // during the drain and nothing to suppress.
494        ctx.obligation.record_settled();
495    }
496
497    /// Note that a start has made this timer live, so `Drop` owes a drain.
498    ///
499    /// One site for the `start*` methods, which differ only in how they compute
500    /// the first due time. `start` is not among them: it delegates to
501    /// `start_after`.
502    fn record_live_before(&self, publish: impl FnOnce()) {
503        // SAFETY: the context outlives every tick and is freed only by Drop,
504        // which cannot run while this borrow of self is alive.
505        unsafe { &*self.context }
506            .obligation
507            .record_live_before(publish);
508    }
509
510    /// Give up ownership, returning the raw object, its callback context, and
511    /// the period.
512    ///
513    /// Used only by [`crate::cleanup_group::CleanupGroup`], which takes over the
514    /// first two: a group member is released by
515    /// `CloseThreadpoolCleanupGroupMembers` and must not close itself, so this
516    /// suppresses this type's `Drop`.
517    pub(crate) fn into_parts(self) -> (PTP_TIMER, *mut core::ffi::c_void, Duration) {
518        let this = std::mem::ManuallyDrop::new(self);
519        (this.timer, this.context.cast(), this.period)
520    }
521
522    /// Free a context returned by [`ThreadpoolPeriodicTimer::into_parts`].
523    ///
524    /// # Safety
525    ///
526    /// `context` must come from `into_parts` on this type, its object must
527    /// already have been released, and it must be freed exactly once.
528    pub(crate) unsafe fn drop_context(context: *mut core::ffi::c_void) {
529        // SAFETY: forwarded from this function's own contract.
530        drop(unsafe { Box::from_raw(context.cast::<PeriodicContext>()) });
531    }
532
533    /// Whether `Drop` would report an undischarged drain obligation right now.
534    ///
535    /// Exists so the obligation's wiring can be asserted without depending on
536    /// the trace, whose filter is fixed before `main` and so cannot be narrowed
537    /// from inside a test.
538    #[cfg(test)]
539    pub(crate) fn obligation_owed(&self) -> bool {
540        // SAFETY: the context outlives every tick and is freed only by Drop.
541        unsafe { &*self.context }.obligation.is_owed()
542    }
543}
544
545impl Drop for ThreadpoolPeriodicTimer {
546    fn drop(&mut self) {
547        // Stop before draining, or the timer would queue a fresh tick while the
548        // drain is in progress and never settle.
549        crate::trace_record!("timer-periodic", "drop-begin", self.timer);
550        // Read before the stop and drain, and necessarily so: `stop_and_drain`
551        // below settles the obligation, so asking afterwards would always find
552        // nothing owed.
553        // SAFETY: the context is still live; it is freed at the end of this body.
554        // Captured, not re-read later: `stop_and_drain` below settles the
555        // obligation by itself, and the context is freed before the fail-fast,
556        // so this must be a value rather than a borrow.
557        // SAFETY: the context is still live; it is freed at the end of this body.
558        let owed = unsafe { &*self.context }.obligation.is_owed();
559        if owed {
560            crate::trace_record!(
561                "timer-periodic",
562                crate::obligation::DROP_OBLIGATION_OWED,
563                self.timer
564            );
565        }
566        self.stop_and_drain();
567        crate::trace_record!("timer-periodic", "drop-drained", self.timer);
568
569        // SAFETY: no tick can be queued or executing, so the object can be
570        // closed and the context freed exactly once.
571        crate::trace_call!("CloseThreadpoolTimer", self.timer, 0, {
572            // SAFETY: no tick remains, so the object can be closed once.
573            unsafe { CloseThreadpoolTimer(self.timer) };
574        });
575        // SAFETY: nothing can reach the context again; free it exactly once.
576        unsafe { drop(Box::from_raw(self.context)) };
577        crate::trace_record!("timer-periodic", "drop-closed", self.timer);
578        // Last, after the drain, the close and the context free: a panic
579        // unwinds, so anything after it would be skipped.
580        crate::obligation::fail_fast_if_owed(owed, "ThreadpoolPeriodicTimer", "stop_and_drain");
581    }
582}
583
584impl std::fmt::Debug for ThreadpoolPeriodicTimer {
585    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
586        f.debug_struct("ThreadpoolPeriodicTimer")
587            .field("period", &self.period)
588            .field("is_running", &self.is_running())
589            .finish_non_exhaustive()
590    }
591}
592
593#[cfg(test)]
594mod tests;