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;