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, TRUE};
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    callback: Box<dyn Fn(&PeriodicTick<'_>) + Send + Sync + 'static>,
25}
26
27/// One tick of a [`ThreadpoolPeriodicTimer`], handed to its callback.
28///
29/// A tick may be running concurrently with other ticks of the same timer, so
30/// anything this callback touches must tolerate that.
31pub struct PeriodicTick<'ctx> {
32    ctx: &'ctx PeriodicContext,
33}
34
35impl std::fmt::Debug for PeriodicTick<'_> {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        f.debug_struct("PeriodicTick").finish_non_exhaustive()
38    }
39}
40
41impl PeriodicTick<'_> {
42    /// Stop the timer from inside its own callback.
43    ///
44    /// This is how a periodic timer ends itself -- "tick until some condition
45    /// holds" needs no external coordination.
46    ///
47    /// It stops *future* ticks being queued. It does not retract ticks already
48    /// queued, and it does not affect ticks already running, including other
49    /// concurrent runs of this same callback. Expect the callback to run again
50    /// after calling this, and make it idempotent accordingly.
51    pub fn stop(&self) {
52        let timer = self.ctx.timer.load(Ordering::Acquire);
53        debug_assert_ne!(timer, 0, "the timer must be published before callbacks");
54        // SAFETY: `timer` is this object's live PTP_TIMER, published before any
55        // callback could run.
56        unsafe { disarm_raw(timer) };
57    }
58}
59
60/// Trampoline from the raw `PTP_TIMER_CALLBACK` ABI into the boxed closure.
61///
62/// SAFETY: `context` must point to a live [`PeriodicContext`] for the entire
63/// duration of every callback invocation, which [`ThreadpoolPeriodicTimer`]'s `Drop`
64/// ordering guarantees.
65unsafe extern "system" fn periodic_trampoline(
66    _instance: PTP_CALLBACK_INSTANCE,
67    context: *mut core::ffi::c_void,
68    _timer: PTP_TIMER,
69) {
70    // SAFETY: context is a valid *mut PeriodicContext for the full callback duration.
71    let ctx = unsafe { &*(context as *const PeriodicContext) };
72    let tick = PeriodicTick { ctx };
73    // Not contained: see the callback contract in the crate docs.
74    (ctx.callback)(&tick);
75}
76
77/// An owned repeating thread-pool timer.
78///
79/// The period is fixed when the timer is created, so the type says what it is:
80/// this object exists to tick on a cadence, and there is no argument that can
81/// quietly turn it into a one-shot.
82///
83/// # Ticks can overlap
84///
85/// **The pool queues each tick on schedule regardless of whether the previous
86/// tick has finished.** If the callback takes longer than the period, two or
87/// more runs of it will execute concurrently on different pool threads. This is
88/// the property that makes periodic timers surprising in practice, and it
89/// follows from the cadence being fixed: the schedule cannot wait for the
90/// callback without ceasing to be a schedule.
91///
92/// So a `ThreadpoolPeriodicTimer` callback must be safe to run concurrently with itself.
93/// If that is awkward, the alternative is a one-shot [`ThreadpoolTimer`](crate::timer::ThreadpoolTimer) re-armed from
94/// inside its own callback with [`crate::timer::TimerFiring::rearm_after`]:
95/// there is never more than one firing outstanding, and the gap is measured from
96/// the end of each firing rather than from a fixed schedule.
97///
98/// |  | [`ThreadpoolPeriodicTimer`] | [`ThreadpoolTimer`](crate::timer::ThreadpoolTimer) + `rearm_after` |
99/// |---|---|---|
100/// | Cadence | fixed, independent of callback duration | drifts by the callback duration |
101/// | Concurrent runs of the callback | possible | never |
102/// | Slow callback | ticks pile up and overlap | next tick simply happens later |
103///
104/// # Teardown
105///
106/// [`Drop`] stops the timer before draining callbacks, so it cannot requeue
107/// during teardown. [`ThreadpoolPeriodicTimer::stop_and_drain`] does the same thing under
108/// the caller's control, and is the ordering to copy if doing it by hand:
109/// stop first, drain second.
110///
111/// # Examples
112///
113/// ```
114/// use std::sync::Arc;
115/// use std::sync::atomic::{AtomicUsize, Ordering};
116/// use std::time::Duration;
117/// use windows_threadpool_sys::timer::ThreadpoolPeriodicTimer;
118///
119/// let ticks = Arc::new(AtomicUsize::new(0));
120/// let counter = Arc::clone(&ticks);
121///
122/// // The period belongs to the timer, not to a call.
123/// let timer = ThreadpoolPeriodicTimer::new(Duration::from_millis(1), move |_tick| {
124///     counter.fetch_add(1, Ordering::SeqCst);
125/// }, None)?;
126///
127/// timer.start();
128/// while ticks.load(Ordering::SeqCst) < 3 {
129///     std::thread::yield_now();
130/// }
131///
132/// timer.stop_and_drain();
133/// assert!(ticks.load(Ordering::SeqCst) >= 3);
134/// # Ok::<(), std::io::Error>(())
135/// ```
136///
137/// Stopping from inside the callback, for "tick until done". Note the counter
138/// may pass the threshold, because a tick already queued still runs:
139///
140/// ```
141/// use std::sync::Arc;
142/// use std::sync::atomic::{AtomicUsize, Ordering};
143/// use std::time::Duration;
144/// use windows_threadpool_sys::timer::ThreadpoolPeriodicTimer;
145///
146/// let ticks = Arc::new(AtomicUsize::new(0));
147/// let counter = Arc::clone(&ticks);
148///
149/// let timer = ThreadpoolPeriodicTimer::new(Duration::from_millis(1), move |tick| {
150///     if counter.fetch_add(1, Ordering::SeqCst) >= 2 {
151///         tick.stop();
152///     }
153/// }, None)?;
154///
155/// timer.start();
156/// while timer.is_running() {
157///     std::thread::yield_now();
158/// }
159/// timer.stop_and_drain();
160/// assert!(ticks.load(Ordering::SeqCst) >= 3);
161/// # Ok::<(), std::io::Error>(())
162/// ```
163pub struct ThreadpoolPeriodicTimer {
164    timer: PTP_TIMER,
165    period: Duration,
166    // Kept alive as a raw pointer until Drop has stopped and drained.
167    context: *mut PeriodicContext,
168}
169
170// SAFETY: PTP_TIMER is a cross-thread pool object, and the context holds a
171// callback that is Fn + Send + Sync; the pointer is only read until Drop frees
172// it after all callbacks have finished.
173unsafe impl Send for ThreadpoolPeriodicTimer {}
174unsafe impl Sync for ThreadpoolPeriodicTimer {}
175
176/// Nanoseconds in a millisecond, for checking a period divides evenly.
177const NANOS_PER_MILLI: u32 = 1_000_000;
178
179impl ThreadpoolPeriodicTimer {
180    /// The shortest period this timer can express: one millisecond.
181    ///
182    /// `SetThreadpoolTimer` takes the period as whole milliseconds, so nothing
183    /// shorter can be represented. This is a floor on what may be *asked for*,
184    /// not a promise about delivery: ticks arrive on the system timer tick,
185    /// which is far coarser (~15.6ms by default).
186    pub const MIN_PERIOD: Duration = Duration::from_millis(1);
187
188    /// The longest period this timer can express: `u32::MAX` milliseconds, or
189    /// just under 50 days.
190    ///
191    /// The pool's period field is a `u32` count of milliseconds, so a longer
192    /// period cannot be represented.
193    pub const MAX_PERIOD: Duration = Duration::from_millis(u32::MAX as u64);
194
195    /// Create a stopped timer that invokes `callback` every `period`.
196    ///
197    /// `period` must be a whole number of milliseconds between
198    /// [`MIN_PERIOD`](Self::MIN_PERIOD) and [`MAX_PERIOD`](Self::MAX_PERIOD);
199    /// anything else is rejected. The pool takes the period as a `u32` count of
200    /// milliseconds, so every other value would be quietly altered to fit:
201    ///
202    /// - a period below a millisecond rounds down to zero, and a zero period
203    ///   means "do not repeat" -- the timer would fire once and stop, which is a
204    ///   `ThreadpoolPeriodicTimer` silently behaving like a
205    ///   [`ThreadpoolTimer`](crate::timer::ThreadpoolTimer);
206    /// - a fractional period such as 1.5ms is truncated, so the timer would tick
207    ///   at 1ms while [`period`](Self::period) still reported 1.5ms;
208    /// - a period beyond `u32::MAX` milliseconds would be capped, ticking far
209    ///   more often than asked.
210    ///
211    /// Rejecting rather than rounding keeps [`period`](Self::period) an accurate
212    /// report of what was scheduled.
213    ///
214    /// Pass `Some(env)` to select a private pool or callback priority; `None`
215    /// uses the process-default pool with default priority.
216    ///
217    /// The callback runs on a shared, process-managed pool thread, may run
218    /// concurrently with itself (see the type documentation), must restore any
219    /// thread state it changes, and must not terminate its thread. It must not
220    /// panic: a panic unwinds to the `extern "system"` trampoline and aborts the
221    /// process.
222    ///
223    /// # Errors
224    ///
225    /// Returns [`io::ErrorKind::InvalidInput`] if `period` is outside
226    /// [`MIN_PERIOD`](Self::MIN_PERIOD)..=[`MAX_PERIOD`](Self::MAX_PERIOD) or is
227    /// not a whole number of milliseconds, or the error from
228    /// `CreateThreadpoolTimer`.
229    pub fn new<F>(
230        period: Duration,
231        callback: F,
232        env: Option<&mut CallbackEnviron>,
233    ) -> io::Result<Self>
234    where
235        F: Fn(&PeriodicTick<'_>) + Send + Sync + 'static,
236    {
237        if period < Self::MIN_PERIOD {
238            return Err(io::Error::new(
239                io::ErrorKind::InvalidInput,
240                "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",
241            ));
242        }
243        if period > Self::MAX_PERIOD {
244            return Err(io::Error::new(
245                io::ErrorKind::InvalidInput,
246                "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",
247            ));
248        }
249        if !period.subsec_nanos().is_multiple_of(NANOS_PER_MILLI) {
250            return Err(io::Error::new(
251                io::ErrorKind::InvalidInput,
252                "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",
253            ));
254        }
255
256        let context = Box::into_raw(Box::new(PeriodicContext {
257            timer: AtomicIsize::new(0),
258            callback: Box::new(callback),
259        }));
260        let env_ptr = env.map_or(ptr::null_mut(), |e| e.as_mut_ptr());
261
262        // SAFETY: context is a valid heap pointer that outlives every callback,
263        // and env_ptr is valid (or null) for the duration of this call.
264        let timer = unsafe {
265            CreateThreadpoolTimer(
266                Some(periodic_trampoline),
267                context.cast(),
268                env_ptr.cast_const(),
269            )
270        };
271
272        if timer == 0 {
273            let error = io::Error::last_os_error();
274            // SAFETY: the pool never saw context; reclaim it immediately.
275            unsafe { drop(Box::from_raw(context)) };
276            return Err(error);
277        }
278
279        // Publish the object before any callback can run. The timer is not
280        // started yet, so no callback can observe the unpublished value.
281        // SAFETY: context is live and exclusively ours until the first start.
282        unsafe { (*context).timer.store(timer, Ordering::Release) };
283
284        Ok(Self {
285            timer,
286            period,
287            context,
288        })
289    }
290
291    /// The period this timer ticks on.
292    #[must_use]
293    pub fn period(&self) -> Duration {
294        self.period
295    }
296
297    /// Start ticking, with the first tick one period from now.
298    pub fn start(&self) {
299        self.start_after(self.period);
300    }
301
302    /// Start ticking, with the first tick `first_delay` from now.
303    ///
304    /// Subsequent ticks follow every [`ThreadpoolPeriodicTimer::period`]. A zero
305    /// `first_delay` makes the first tick due immediately.
306    pub fn start_after(&self, first_delay: Duration) {
307        // SAFETY: timer is valid for the lifetime of self.
308        unsafe {
309            arm_raw(
310                self.timer,
311                relative_filetime(first_delay),
312                millis_u32(self.period),
313                0,
314            );
315        }
316    }
317
318    /// Start ticking, with the first tick at the wall-clock instant `when`.
319    ///
320    /// Unlike a relative first delay, an absolute one passes through sleep and
321    /// hibernation.
322    pub fn start_at(&self, when: SystemTime) {
323        // SAFETY: timer is valid for the lifetime of self.
324        unsafe {
325            arm_raw(
326                self.timer,
327                absolute_filetime(when),
328                millis_u32(self.period),
329                0,
330            );
331        }
332    }
333
334    /// Start ticking, allowing the system a coalescing `window` on each tick.
335    ///
336    /// `window` is the tolerance the system may add so it can group this timer
337    /// with other expirations and wake the processor less often, trading timing
338    /// precision for power.
339    pub fn start_with_window(&self, first_delay: Duration, window: Duration) {
340        // SAFETY: timer is valid for the lifetime of self.
341        unsafe {
342            arm_raw(
343                self.timer,
344                relative_filetime(first_delay),
345                millis_u32(self.period),
346                millis_u32(window),
347            );
348        }
349    }
350
351    /// Stop the timer.
352    ///
353    /// Future ticks stop being queued, but a tick already queued still runs and
354    /// ticks already executing are unaffected. Use
355    /// [`ThreadpoolPeriodicTimer::stop_and_drain`] to also wait for those.
356    pub fn stop(&self) {
357        // SAFETY: timer is valid for the lifetime of self.
358        unsafe { disarm_raw(self.timer) };
359    }
360
361    /// Whether the timer is currently started.
362    ///
363    /// Ticking does not clear the schedule, so this stays `true` until something
364    /// stops the timer -- [`ThreadpoolPeriodicTimer::stop`], [`PeriodicTick::stop`], or
365    /// teardown.
366    #[must_use]
367    pub fn is_running(&self) -> bool {
368        // SAFETY: timer is valid for the lifetime of self.
369        unsafe { IsThreadpoolTimerSet(self.timer) != 0 }
370    }
371
372    /// Block until all queued and executing ticks have completed.
373    ///
374    /// Stop the timer first, or this waits for a schedule that keeps producing
375    /// new ticks. [`ThreadpoolPeriodicTimer::stop_and_drain`] does both in the right order.
376    pub fn wait(&self) {
377        // SAFETY: timer is valid for the lifetime of self.
378        unsafe { WaitForThreadpoolTimerCallbacks(self.timer, FALSE) };
379    }
380
381    /// Stop the timer and wait until no tick is queued or executing.
382    ///
383    /// This is the correct teardown order -- stop first, drain second -- and is
384    /// what [`Drop`] performs. Ticks that have not started are dropped rather
385    /// than run.
386    ///
387    /// The result holds provided no other thread starts the timer during the
388    /// call. `ThreadpoolPeriodicTimer` is `Sync` and the `start*` methods take
389    /// `&self`, so a start landing between the stop and the drain is not
390    /// excluded by anything here, and would leave a schedule installed on
391    /// return. A caller needing the timer to be provably stopped must own it
392    /// exclusively or serialize access to it. Unlike the one-shot timer there is
393    /// no re-arm to suppress: [`PeriodicTick::stop`] only ever stops.
394    pub fn stop_and_drain(&self) {
395        self.stop();
396        // SAFETY: timer is valid for the lifetime of self. A cancelled timer
397        // callback owns no storage, so dropping queued ticks orphans nothing.
398        unsafe { WaitForThreadpoolTimerCallbacks(self.timer, TRUE) };
399    }
400
401    /// Give up ownership, returning the raw object, its callback context, and
402    /// the period.
403    ///
404    /// Used only by [`crate::cleanup_group::CleanupGroup`], which takes over the
405    /// first two: a group member is released by
406    /// `CloseThreadpoolCleanupGroupMembers` and must not close itself, so this
407    /// suppresses this type's `Drop`.
408    pub(crate) fn into_parts(self) -> (PTP_TIMER, *mut core::ffi::c_void, Duration) {
409        let this = std::mem::ManuallyDrop::new(self);
410        (this.timer, this.context.cast(), this.period)
411    }
412
413    /// Free a context returned by [`ThreadpoolPeriodicTimer::into_parts`].
414    ///
415    /// # Safety
416    ///
417    /// `context` must come from `into_parts` on this type, its object must
418    /// already have been released, and it must be freed exactly once.
419    pub(crate) unsafe fn drop_context(context: *mut core::ffi::c_void) {
420        // SAFETY: forwarded from this function's own contract.
421        drop(unsafe { Box::from_raw(context.cast::<PeriodicContext>()) });
422    }
423}
424
425impl Drop for ThreadpoolPeriodicTimer {
426    fn drop(&mut self) {
427        // Stop before draining, or the timer would queue a fresh tick while the
428        // drain is in progress and never settle.
429        self.stop_and_drain();
430
431        // SAFETY: no tick can be queued or executing, so the object can be
432        // closed and the context freed exactly once.
433        unsafe {
434            CloseThreadpoolTimer(self.timer);
435            drop(Box::from_raw(self.context));
436        }
437    }
438}
439
440impl std::fmt::Debug for ThreadpoolPeriodicTimer {
441    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442        f.debug_struct("ThreadpoolPeriodicTimer")
443            .field("period", &self.period)
444            .field("is_running", &self.is_running())
445            .finish_non_exhaustive()
446    }
447}
448
449#[cfg(test)]
450mod tests;