Skip to main content

windows_threadpool_sys/
cleanup_group.rs

1// Copyright (c) 2026 Mike Grier
2//! Cleanup groups: releasing many thread-pool objects in one step.
3//!
4//! A cleanup group tears down every object created into it with a single
5//! `CloseThreadpoolCleanupGroupMembers`, which waits for executing callbacks and
6//! (optionally) cancels those that have not started. That is the SDK's answer to
7//! shutting down a subsystem without tracking each object individually.
8//!
9//! # Why members are created *by* the group
10//!
11//! Releasing members is bulk and irreversible: afterwards a member must not be
12//! used or closed again, and only then is its heap callback context safe to
13//! free. An individually-owned object cannot know when that has happened, so the
14//! group owns both the members and their contexts.
15//!
16//! That ownership is expressed in the types. Members borrow the group, and
17//! [`CleanupGroup::close_members`] takes `&mut self`, so the borrow checker
18//! rejects any use of a member after the group has released it:
19//!
20//! ```compile_fail,E0502
21//! # use windows_threadpool_sys::cleanup_group::CleanupGroup;
22//! let mut group = CleanupGroup::new().expect("create group");
23//! let work = group.create_work(|| {}, None).expect("create work");
24//! group.close_members();
25//! work.submit(); // error: `group` is mutably borrowed above
26//! ```
27//!
28//! # Thread-pool I/O is deliberately excluded
29//!
30//! There is no `create_io`. A `TP_IO` object must not be closed while any
31//! overlapped operation is outstanding, because the kernel still owns that
32//! operation's storage -- and a cleanup group's bulk release has no way to
33//! satisfy that precondition for its members. [`crate::io::ThreadpoolIo`]
34//! therefore stays individually owned, where its `Drop` can cancel, drain, and
35//! only then close. Grouping it would trade a guarantee for a convenience.
36
37use core::ffi::c_void;
38use std::io;
39use std::marker::PhantomData;
40use std::os::windows::io::BorrowedHandle;
41use std::ptr;
42use std::sync::Mutex;
43use std::time::{Duration, SystemTime};
44
45use windows_sys::Win32::Foundation::{FALSE, TRUE};
46use windows_sys::Win32::System::Threading::{
47    CloseThreadpoolCleanupGroup, CloseThreadpoolCleanupGroupMembers, CreateThreadpoolCleanupGroup,
48    IsThreadpoolTimerSet, PTP_CLEANUP_GROUP, PTP_TIMER, PTP_WAIT, PTP_WORK, SubmitThreadpoolWork,
49    WaitForThreadpoolTimerCallbacks, WaitForThreadpoolWaitCallbacks,
50    WaitForThreadpoolWorkCallbacks,
51};
52
53use crate::callback_env::CallbackEnviron;
54use crate::timer::{
55    PeriodicTick, ThreadpoolPeriodicTimer, ThreadpoolTimer, TimerFiring, absolute_filetime,
56    arm_raw, disarm_raw, millis_u32, relative_filetime,
57};
58use crate::wait::{ThreadpoolWait, WaitActivation, WaitTarget, WaitableHandle};
59use crate::work::ThreadpoolWork;
60
61/// A heap allocation the group frees once its members have been released.
62///
63/// Resources are type-erased because one group holds members of several kinds;
64/// each entry carries the function that knows how to free it, and the function
65/// that prepares its member for the bulk release.
66struct OwnedResource {
67    ptr: *mut c_void,
68    /// Suppress the member's deferred re-arm and disarm it before
69    /// `CloseThreadpoolCleanupGroupMembers` runs. A no-op for kinds with no
70    /// callback-driven re-arm (work, periodic timers, watched handles).
71    prepare_shutdown: unsafe fn(*mut c_void),
72    /// Take a repair claim on this member's pool while it is still live.
73    ///
74    /// Yields nothing for every kind but a wait: the removal that can sever a
75    /// pool's arrival notification operates on a wait completion packet, and
76    /// only a wait owns one.
77    ///
78    /// Returns the claim, which the caller holds across the native release so
79    /// the pool cannot be freed under the marking pass that follows.
80    recover_repair: unsafe fn(*mut c_void) -> Option<crate::heal::Registration>,
81    free: unsafe fn(*mut c_void),
82}
83
84/// A repair hook for a member whose release cannot wedge a pool.
85///
86/// Returning `None` is what keeps such a member out of the marking pass
87/// entirely: the claims are now what that pass iterates, so a kind that cannot
88/// owe a repair simply produces no claim. There used to be a second hook
89/// reporting the same fact a different way, and the two could disagree.
90///
91/// SAFETY: takes a pointer it never dereferences.
92unsafe fn no_repair_recovery(_ptr: *mut c_void) -> Option<crate::heal::Registration> {
93    None
94}
95
96// SAFETY: each pointer is a `Box` the group exclusively owns and frees exactly
97// once, after the pool has released every member that could reach it.
98unsafe impl Send for OwnedResource {}
99
100/// Free a boxed value the group owns directly, rather than a callback context.
101///
102/// SAFETY: `ptr` must be a `Box<T>` reclaimed exactly once.
103unsafe fn free_boxed<T>(ptr: *mut c_void) {
104    // SAFETY: forwarded from this function's own contract.
105    drop(unsafe { Box::from_raw(ptr.cast::<T>()) });
106}
107
108/// A shutdown preparation for a member with no callback-driven re-arm to
109/// suppress: work objects, periodic timers, and watched handles.
110///
111/// `CloseThreadpoolCleanupGroupMembers` already disarms and cancels these; only
112/// a one-shot timer or a wait can re-arm itself from inside a callback, so only
113/// those need the real preparation.
114fn prepare_shutdown_noop(_ptr: *mut c_void) {}
115
116/// An owned thread-pool cleanup group.
117///
118/// Create members with [`CleanupGroup::create_work`],
119/// [`CleanupGroup::create_timer`], [`CleanupGroup::create_periodic_timer`], and
120/// [`CleanupGroup::create_wait`], then release them all with
121/// [`CleanupGroup::close_members`]. `Drop` releases any members that are still
122/// open, so forgetting to call `close_members` is safe -- it only gives up
123/// control over *when* the teardown blocks.
124///
125/// # Examples
126///
127/// ```
128/// use std::sync::Arc;
129/// use std::sync::atomic::{AtomicUsize, Ordering};
130/// use std::time::Duration;
131/// use windows_threadpool_sys::cleanup_group::CleanupGroup;
132///
133/// let count = Arc::new(AtomicUsize::new(0));
134/// let work_counter = Arc::clone(&count);
135/// let timer_counter = Arc::clone(&count);
136///
137/// let mut group = CleanupGroup::new()?;
138/// {
139///     let work = group.create_work(move || {
140///         work_counter.fetch_add(1, Ordering::SeqCst);
141///     }, None)?;
142///     let timer = group.create_timer(move |_firing| {
143///         timer_counter.fetch_add(1, Ordering::SeqCst);
144///     }, None)?;
145///
146///     work.submit();
147///     timer.set_after(Duration::from_millis(1));
148///
149///     // Wait for the work to have run and the timer to have fired. Note that
150///     // `timer.wait()` would not do: it waits for callbacks the pool has
151///     // already queued, and a timer that has not expired yet has none.
152///     while count.load(Ordering::SeqCst) < 2 {
153///         std::thread::yield_now();
154///     }
155/// }
156///
157/// // One call tears down every member of the group.
158/// group.close_members();
159/// assert_eq!(count.load(Ordering::SeqCst), 2);
160/// # Ok::<(), std::io::Error>(())
161/// ```
162pub struct CleanupGroup {
163    group: PTP_CLEANUP_GROUP,
164    /// Contexts and handles owned on behalf of members, freed after release.
165    ///
166    /// This is the only record of what is outstanding, and it is deliberately
167    /// not paired with a "already released" flag. Such a flag would latch: the
168    /// `create_*` methods take `&self`, so members can be created after a
169    /// release returns, and a latched release would then skip them -- leaking
170    /// their contexts and closing the group with live members.
171    resources: Mutex<Vec<OwnedResource>>,
172    /// Test-only: run between preparing the members and releasing them.
173    ///
174    /// Stands in for the self-heal's tick arriving in that interval, which is
175    /// the only way to observe *when* a cancelling release marks its pools.
176    /// Both orderings leave a mark outstanding once the release has returned,
177    /// so an end-state assertion cannot tell them apart; something has to act
178    /// inside the window. Per-group rather than a static, because `cargo test`
179    /// runs these as threads of one process.
180    #[cfg(test)]
181    before_release: Mutex<Option<Box<dyn Fn() + Send + Sync>>>,
182}
183
184// SAFETY: PTP_CLEANUP_GROUP is a kernel-managed object usable from any thread,
185// and the resource list is mutex-guarded.
186unsafe impl Send for CleanupGroup {}
187unsafe impl Sync for CleanupGroup {}
188
189impl CleanupGroup {
190    /// Create an empty cleanup group.
191    ///
192    /// # Errors
193    ///
194    /// Returns the error from `CreateThreadpoolCleanupGroup`.
195    pub fn new() -> io::Result<Self> {
196        // SAFETY: the call takes no inputs.
197        let group = crate::trace_call!("CreateThreadpoolCleanupGroup", 0, 0, {
198            // SAFETY: the call takes no inputs.
199            unsafe { CreateThreadpoolCleanupGroup() }
200        });
201        if group == 0 {
202            return Err(io::Error::last_os_error());
203        }
204        Ok(Self {
205            group,
206            resources: Mutex::new(Vec::new()),
207            #[cfg(test)]
208            before_release: Mutex::new(None),
209        })
210    }
211
212    /// Install the test-only hook that runs just before the native release.
213    ///
214    /// See the field's documentation for why observing that instant is the
215    /// only way to pin the order in which a cancelling release marks its
216    /// pools.
217    ///
218    /// Carries the feature condition of its only caller, which is the test for
219    /// that marking order: without `self-heal` there is no mark to observe, so
220    /// an ungated hook here is dead code in a `--no-default-features` build --
221    /// which CI compiles with `-D warnings`.
222    #[cfg(all(test, feature = "self-heal"))]
223    pub(crate) fn on_before_release(&self, hook: impl Fn() + Send + Sync + 'static) {
224        *self
225            .before_release
226            .lock()
227            .unwrap_or_else(|poison| poison.into_inner()) = Some(Box::new(hook));
228    }
229
230    /// Build the environment a member is created with, layering this group on
231    /// top of whatever pool and priority the caller chose.
232    ///
233    /// The caller's environment is copied rather than mutated, so passing one
234    /// environment to several groups -- or reusing it for a non-member object --
235    /// behaves as written.
236    fn member_environment(&self, env: Option<&CallbackEnviron<'_>>) -> CallbackEnviron<'_> {
237        let mut member_env = match env {
238            Some(env) => CallbackEnviron::from_inner(*env.as_inner()),
239            None => CallbackEnviron::new(),
240        };
241        // SAFETY: `self.group` is live for at least as long as the member being
242        // created, because the member borrows this group, and the member is
243        // never closed individually -- `close_members` releases it.
244        unsafe { member_env.set_cleanup_group(self.group, None) };
245        member_env
246    }
247
248    fn adopt(&self, resource: OwnedResource) {
249        self.resources
250            .lock()
251            .unwrap_or_else(|poison| poison.into_inner())
252            .push(resource);
253    }
254
255    /// Create a work object owned by this group.
256    ///
257    /// Equivalent to [`ThreadpoolWork::new`], except that the returned member is
258    /// released by [`CleanupGroup::close_members`] rather than by its own drop.
259    ///
260    /// # Errors
261    ///
262    /// Returns the error from `CreateThreadpoolWork`.
263    pub fn create_work<F>(
264        &self,
265        callback: F,
266        env: Option<&CallbackEnviron>,
267    ) -> io::Result<WorkMember<'_>>
268    where
269        F: Fn() + Send + Sync + 'static,
270    {
271        let mut member_env = self.member_environment(env);
272        let work = ThreadpoolWork::new(callback, Some(&mut member_env))?;
273        let (handle, context) = work.into_parts();
274        self.adopt(OwnedResource {
275            ptr: context,
276            prepare_shutdown: prepare_shutdown_noop,
277            recover_repair: no_repair_recovery,
278            free: ThreadpoolWork::drop_context,
279        });
280        Ok(WorkMember {
281            handle,
282            _group: PhantomData,
283        })
284    }
285
286    /// Create a one-shot timer owned by this group.
287    ///
288    /// Equivalent to [`ThreadpoolTimer::new`].
289    ///
290    /// # Errors
291    ///
292    /// Returns the error from `CreateThreadpoolTimer`.
293    pub fn create_timer<F>(
294        &self,
295        callback: F,
296        env: Option<&CallbackEnviron>,
297    ) -> io::Result<TimerMember<'_>>
298    where
299        F: Fn(&TimerFiring<'_>) + Send + Sync + 'static,
300    {
301        let mut member_env = self.member_environment(env);
302        let timer = ThreadpoolTimer::new(callback, Some(&mut member_env))?;
303        let (handle, context) = timer.into_parts();
304        self.adopt(OwnedResource {
305            ptr: context,
306            prepare_shutdown: ThreadpoolTimer::prepare_shutdown,
307            recover_repair: no_repair_recovery,
308            free: ThreadpoolTimer::drop_context,
309        });
310        Ok(TimerMember {
311            handle,
312            context,
313            _group: PhantomData,
314        })
315    }
316
317    /// Create a periodic timer owned by this group.
318    ///
319    /// Equivalent to [`ThreadpoolPeriodicTimer::new`], including that its ticks
320    /// may overlap one another.
321    ///
322    /// # Errors
323    ///
324    /// Returns [`io::ErrorKind::InvalidInput`] if `period` is outside
325    /// [`ThreadpoolPeriodicTimer::MIN_PERIOD`]..=[`ThreadpoolPeriodicTimer::MAX_PERIOD`]
326    /// or is not a whole number of milliseconds, or the error from
327    /// `CreateThreadpoolTimer`.
328    pub fn create_periodic_timer<F>(
329        &self,
330        period: Duration,
331        callback: F,
332        env: Option<&CallbackEnviron>,
333    ) -> io::Result<PeriodicTimerMember<'_>>
334    where
335        F: Fn(&PeriodicTick<'_>) + Send + Sync + 'static,
336    {
337        let mut member_env = self.member_environment(env);
338        let timer = ThreadpoolPeriodicTimer::new(period, callback, Some(&mut member_env))?;
339        let (handle, context, period) = timer.into_parts();
340        self.adopt(OwnedResource {
341            ptr: context,
342            prepare_shutdown: prepare_shutdown_noop,
343            recover_repair: no_repair_recovery,
344            free: ThreadpoolPeriodicTimer::drop_context,
345        });
346        Ok(PeriodicTimerMember {
347            handle,
348            period,
349            context,
350            _group: PhantomData,
351        })
352    }
353
354    /// Create a wait object owned by this group, watching `handle`.
355    ///
356    /// The group takes ownership of the handle as well as the object, because
357    /// the pool may still be watching it until the members are released.
358    ///
359    /// Like [`ThreadpoolWait::new`], this takes a [`WaitableHandle`] rather than
360    /// a bare handle, so the group path cannot reach the unsupported wait
361    /// targets that the individually-owned path rejects.
362    ///
363    /// # Errors
364    ///
365    /// Returns the error from `CreateThreadpoolWait`.
366    pub fn create_wait<F>(
367        &self,
368        handle: WaitableHandle,
369        callback: F,
370        env: Option<&CallbackEnviron<'_>>,
371    ) -> io::Result<WaitMember<'_>>
372    where
373        F: Fn(&WaitActivation<'_>) + Send + Sync + 'static,
374    {
375        let mut member_env = self.member_environment(env);
376        let wait = ThreadpoolWait::new(handle, callback, Some(&mut member_env))?;
377        let (raw, context, target) = wait.into_parts();
378        self.adopt(OwnedResource {
379            ptr: context,
380            prepare_shutdown: ThreadpoolWait::prepare_shutdown,
381            recover_repair: ThreadpoolWait::recover_repair,
382            free: ThreadpoolWait::drop_context,
383        });
384        // The target outlives the member for the same reason the context does.
385        // Freeing the box runs `WaitTarget`'s drop, which closes the handle with
386        // whichever routine it was built with -- `CloseHandle` for the default
387        // path, the caller's for a custom-close target.
388        let target = Box::into_raw(Box::new(target));
389        self.adopt(OwnedResource {
390            ptr: target.cast(),
391            prepare_shutdown: prepare_shutdown_noop,
392            recover_repair: no_repair_recovery,
393            free: free_boxed::<WaitTarget>,
394        });
395        Ok(WaitMember {
396            handle: raw,
397            watched: target,
398            context,
399            _group: PhantomData,
400        })
401    }
402
403    /// Release every member of this group, letting queued callbacks run.
404    ///
405    /// Waits for executing callbacks to finish; callbacks that have not started
406    /// run before the release completes.
407    ///
408    /// Taking `&mut self` is what makes members unusable afterwards: they borrow
409    /// the group, so the compiler rejects any later use of one. Calling this
410    /// twice is harmless -- the second call finds no members.
411    ///
412    /// The group remains usable afterwards. New members may be created on it,
413    /// and they are released by the next call or by `Drop`, exactly as the first
414    /// batch was.
415    ///
416    /// To drop queued callbacks instead of running them, see
417    /// `close_members_cancelling` -- named without a link because it does not
418    /// exist in a build with `self-heal` off, while this method does, so a link
419    /// would dangle in that configuration.
420    pub fn close_members(&mut self) {
421        self.release_members(false, false);
422    }
423
424    /// Release every member of this group, dropping queued callbacks.
425    ///
426    /// As [`close_members`](Self::close_members), except that callbacks which
427    /// have not started are dropped rather than run.
428    ///
429    /// A cancelling release passes the cancel through to every member, so a wait
430    /// among them reaches the same removal primitive
431    /// [`ThreadpoolWait::try_cancel_pending`](crate::wait::ThreadpoolWait::try_cancel_pending)
432    /// does, and owes its pool the same repair. This crate marks that repair
433    /// here, which is what makes this safe to offer -- subject to the same
434    /// stated hole as the per-object method: a member whose pool could not be
435    /// registered for repair has that registration retried during this call,
436    /// and only if the retry also fails is it cancelled with nothing to repair
437    /// it, recording `cancel-untracked` and panicking under `fail-fast`.
438    ///
439    /// # Availability
440    ///
441    /// Requires the `self-heal` feature, which is on by default. Without it this
442    /// method does not exist and a call to it fails to compile, naming
443    /// [`close_members_cancelling_no_heal_tracking`](Self::close_members_cancelling_no_heal_tracking)
444    /// as what to reach for instead. That is the designed behaviour, not an
445    /// oversight: the repair this method performs is what the feature provides,
446    /// and a compile error is the only way a caller relying on it finds out it
447    /// is gone.
448    #[cfg(feature = "self-heal")]
449    pub fn close_members_cancelling(&mut self) {
450        // The obligation is discharged by `release_members`'s tracking pass,
451        // which marks every member's pool for repair after the native release
452        // and before the contexts are freed.
453        //
454        // This no longer delegates to the `_no_heal_tracking` sibling. It used
455        // to, which made that method's name false: the repair pass was
456        // unconditional, so the "without the repair" method marked pools,
457        // allocated repair objects, started the healer, and could panic under
458        // `fail-fast`. Tracking is now the flag this passes and that one does
459        // not.
460        self.release_members(true, true);
461    }
462
463    /// `close_members_cancelling` without the repair.
464    ///
465    /// Not a link, deliberately: the method it would name does not exist in a
466    /// build with `self-heal` off, and this one does, so the link would dangle
467    /// in exactly the configuration this method exists for.
468    ///
469    /// Always present, including in builds with `self-heal` off, which is the
470    /// point: it is the method that still exists when the gated one does not,
471    /// and its signature says what the caller takes on.
472    ///
473    /// # Safety
474    ///
475    /// The caller must ensure every member's pool is repaired. Submitting any
476    /// work item to a pool does so; so does knowing it is kept live by
477    /// something else. Leaving one unrepaired can stop that pool dispatching --
478    /// which, for the process-default pool, reaches every component in the
479    /// process, including code with no connection to this call.
480    ///
481    /// This is not a memory-safety obligation, and the keyword is not claiming
482    /// one. It is here because the obligation is statable and dischargeable by
483    /// the caller, which is what `unsafe` marks; see
484    /// [the self-heal decision](https://docs.rs/crate/windows-threadpool-sys/latest/source/DESIGN-NOTES.md).
485    pub unsafe fn close_members_cancelling_no_heal_tracking(&mut self) {
486        self.release_members(true, false);
487    }
488
489    /// The number of contexts and handles the group is holding for its members.
490    ///
491    /// Zero once the members have been released.
492    #[must_use]
493    pub fn owned_resources(&self) -> usize {
494        self.resources
495            .lock()
496            .unwrap_or_else(|poison| poison.into_inner())
497            .len()
498    }
499
500    /// Release whatever members exist right now.
501    ///
502    /// Runs in full every time rather than latching after the first call. The
503    /// native release is idempotent -- with no members it does nothing -- and
504    /// running unconditionally is what makes a group usable again afterwards:
505    /// members created after an earlier release are released by the next one,
506    /// instead of being skipped and leaked.
507    fn release_members(&mut self, cancel_pending: bool, track_repairs: bool) {
508        // Close the door on any deferred re-arm before the bulk release.
509        // `CloseThreadpoolCleanupGroupMembers` waits for executing callbacks but
510        // does not stop one from re-arming: a one-shot timer or wait whose
511        // callback is running can request a re-arm the trampoline applies after
512        // it returns, which would re-arm an object the release is tearing down
513        // and then free its context under a freshly queued callback. Suppressing
514        // and disarming each member first mirrors what each object's own `Drop`
515        // does. The lock is dropped before the release, which blocks.
516        //
517        // The repair claims are recovered here too, **before** the release, and
518        // that ordering is a memory-safety requirement rather than a
519        // preference. A member whose pool could not be registered when it was
520        // created holds nothing that keeps that pool alive: `CloseThreadpool`
521        // defers the free until every bound object is gone, and for such a
522        // member the repair work object -- which would be a bound object -- was
523        // never created. The release below frees the last bound object, so by
524        // the time the marking pass runs the pool may already be gone, and the
525        // retry inside `owe_repair` would call `CreateThreadpoolWork` against
526        // freed memory. Claiming here, while the members are still live, both
527        // creates that object and keeps the pool alive across the release.
528        //
529        // The claims are held until after the marking pass and dropped there.
530        let recovered: Vec<crate::heal::Registration> = {
531            let resources = self
532                .resources
533                .lock()
534                .unwrap_or_else(|poison| poison.into_inner());
535            for resource in resources.iter() {
536                // SAFETY: the members are still live and unreleased; each hook
537                // matches the context kind this resource holds and only
538                // suppresses/disarms that one object.
539                unsafe { (resource.prepare_shutdown)(resource.ptr) };
540            }
541            if cancel_pending && track_repairs {
542                resources
543                    .iter()
544                    // SAFETY: as above -- the members are live, and each hook
545                    // reads only the registration of the context kind it
546                    // matches.
547                    .filter_map(|resource| unsafe { (resource.recover_repair)(resource.ptr) })
548                    .collect()
549            } else {
550                Vec::new()
551            }
552        };
553
554        #[cfg(test)]
555        {
556            let hook = self
557                .before_release
558                .lock()
559                .unwrap_or_else(|poison| poison.into_inner());
560            if let Some(hook) = hook.as_ref() {
561                hook();
562            }
563        }
564
565        // The longest-blocking call in this crate: it waits for every member's
566        // executing callback. Bracketed so a release that parks is visible as
567        // an interval rather than inferred from the gap after it.
568        crate::trace_call!(
569            "CloseThreadpoolCleanupGroupMembers",
570            self.group,
571            u32::from(cancel_pending),
572            {
573                // SAFETY: the group is live. This waits for executing callbacks and
574                // releases every member, so afterwards nothing can reach the contexts.
575                unsafe {
576                    CloseThreadpoolCleanupGroupMembers(
577                        self.group,
578                        if cancel_pending { TRUE } else { FALSE },
579                        ptr::null_mut(),
580                    );
581                }
582            }
583        );
584
585        let resources = std::mem::take(
586            &mut *self
587                .resources
588                .lock()
589                .unwrap_or_else(|poison| poison.into_inner()),
590        );
591        let mut tracked = true;
592        if cancel_pending && track_repairs {
593            for claim in recovered.iter() {
594                // A cancelling release passes the cancel through to every
595                // member, so each wait among them reaches the same removal
596                // `ThreadpoolWait::try_cancel_pending` does and owes its pool
597                // the same repair.
598                //
599                // Marked **after** the release returns, matching the standalone
600                // cancel path, and before the contexts are freed below -- which
601                // is why this loop sits between the two rather than beside
602                // either.
603                //
604                // An earlier version marked before the release, justified by
605                // the claim that the native call frees the contexts. It does
606                // not: this function frees them, in the loop immediately after
607                // this one. Marking first left a window in which the healer
608                // could see the mark, find the pool still dispatching, clear it
609                // as repaired, and then have the real cancellation happen with
610                // no mark outstanding -- an unrepaired wedge from a *single*
611                // cancellation, where the race this crate already documents
612                // needs two.
613                //
614                // Marked through the **claims** rather than through the members'
615                // contexts, and that is a safety requirement rather than a
616                // tidier spelling. The context's own registration may be empty,
617                // because registering its pool failed when it was created; the
618                // hook that used to be called here would then retry the
619                // registration, and `CreateThreadpoolWork` would name a pool
620                // whose last bound object the release above has just freed. A
621                // claim that holds an entry is precisely a claim that kept the
622                // pool alive across that release, so marking it is sound; a
623                // claim that holds none reports the cancellation as untracked,
624                // which is what the fail-fast below is for.
625                //
626                // Needs no `unsafe`: a claim owns what it refers to, where the
627                // hook reached through a raw context pointer.
628                //
629                // Accumulated rather than acted on here: the fail-fast this
630                // feeds panics, and a panic raised in this loop would unwind
631                // past the free loop below and leak every member's context.
632                tracked &= claim.owe_repair_claimed();
633            }
634        }
635        // After the marking pass, never before: a claim dropped earlier could
636        // retire the very entry the pass is about to mark. `release` keeps an
637        // entry whose repair is owed, so dropping here cannot.
638        drop(recovered);
639        for resource in resources {
640            // SAFETY: every member has been released, so no callback can still
641            // reach this allocation; each is freed exactly once here.
642            unsafe { (resource.free)(resource.ptr) };
643        }
644        // After the frees, never before: this panics under `fail-fast`, and an
645        // unwind from inside either loop above would skip the frees.
646        //
647        // Only when this release was tracking repairs. The untracked form makes
648        // the repair the caller's problem by contract, so reporting that it did
649        // not happen would be reporting the thing the caller asked for.
650        if track_repairs {
651            crate::obligation::fail_fast_if_untracked(tracked, "CleanupGroup");
652        }
653    }
654}
655
656impl Drop for CleanupGroup {
657    fn drop(&mut self) {
658        // Let queued callbacks run, matching `close_members`.
659        self.release_members(false, false);
660        // SAFETY: the members are released, so the group can be closed.
661        crate::trace_call!("CloseThreadpoolCleanupGroup", self.group, 0, {
662            // SAFETY: the group is live and closed exactly once, here.
663            unsafe { CloseThreadpoolCleanupGroup(self.group) };
664        });
665    }
666}
667
668impl std::fmt::Debug for CleanupGroup {
669    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
670        f.debug_struct("CleanupGroup")
671            .field("owned_resources", &self.owned_resources())
672            .finish_non_exhaustive()
673    }
674}
675
676/// A work object owned by a [`CleanupGroup`].
677///
678/// Behaves like [`ThreadpoolWork`] but is released by the group rather than by
679/// its own drop.
680#[derive(Debug)]
681pub struct WorkMember<'group> {
682    handle: PTP_WORK,
683    _group: PhantomData<&'group CleanupGroup>,
684}
685
686impl WorkMember<'_> {
687    /// Queue one invocation of the callback.
688    pub fn submit(&self) {
689        // SAFETY: the handle is live until the group releases its members,
690        // which the borrow on `_group` prevents from happening first.
691        crate::trace_call!("SubmitThreadpoolWork", self.handle, 0, {
692            // SAFETY: the handle is live until the group releases it.
693            unsafe { SubmitThreadpoolWork(self.handle) };
694        });
695    }
696
697    /// Block until all queued and in-progress invocations have completed.
698    pub fn wait(&self) {
699        // SAFETY: as above.
700        crate::trace_call!("WaitForThreadpoolWorkCallbacks", self.handle, 0, {
701            // SAFETY: the handle is live until the group releases it.
702            unsafe { WaitForThreadpoolWorkCallbacks(self.handle, FALSE) };
703        });
704    }
705
706    /// Stop accepting work and block until none is queued or executing.
707    ///
708    /// As on [`ThreadpoolWork`], there is nothing to *stop* -- a submission
709    /// cannot be withdrawn, only waited for -- so this is exactly
710    /// [`wait`](Self::wait). The name exists so a caller tearing down a mixed
711    /// set of objects can reach for one method.
712    pub fn stop_and_drain(&self) {
713        self.wait();
714    }
715
716    /// Cancel invocations that have not started, then wait for those that have.
717    pub fn cancel_pending(&self) {
718        // SAFETY: as above.
719        crate::trace_call!("WaitForThreadpoolWorkCallbacks(cancel)", self.handle, 1, {
720            // SAFETY: the handle is live until the group releases it.
721            unsafe { WaitForThreadpoolWorkCallbacks(self.handle, TRUE) };
722        });
723    }
724}
725
726/// A one-shot timer owned by a [`CleanupGroup`].
727///
728/// Behaves like [`ThreadpoolTimer`] but is released by the group rather than by
729/// its own drop.
730#[derive(Debug)]
731pub struct TimerMember<'group> {
732    handle: PTP_TIMER,
733    /// The callback context the group owns for this member.
734    ///
735    /// Held so the member can run the same `stop_and_drain` its standalone twin
736    /// does, suppression and all. The group owns and frees it; this is a borrow
737    /// for the member's lifetime.
738    context: *mut c_void,
739    _group: PhantomData<&'group CleanupGroup>,
740}
741
742// SAFETY: the context refers to state the group owns and outlives this member;
743// the member only reads it and passes it to thread-safe pool APIs. The same
744// reasoning as `WaitMember`'s, which is the point -- this type was left without
745// these impls when it gained the pointer, and silently stopped being `Send`.
746unsafe impl Send for TimerMember<'_> {}
747unsafe impl Sync for TimerMember<'_> {}
748
749impl TimerMember<'_> {
750    /// Fire once, `delay` from now.
751    pub fn set_after(&self, delay: Duration) {
752        // SAFETY: the handle is live until the group releases its members.
753        unsafe { arm_raw(self.handle, relative_filetime(delay), 0, 0) };
754    }
755
756    /// Fire once at the wall-clock instant `when`.
757    pub fn set_at(&self, when: SystemTime) {
758        // SAFETY: as above.
759        unsafe { arm_raw(self.handle, absolute_filetime(when), 0, 0) };
760    }
761
762    /// Stop the timer.
763    pub fn disarm(&self) {
764        // SAFETY: as above.
765        unsafe { disarm_raw(self.handle) };
766    }
767
768    /// Whether the timer currently has a due time.
769    ///
770    /// As with [`ThreadpoolTimer::is_set`], expiring does not clear the due
771    /// time; only disarming does.
772    #[must_use]
773    pub fn is_set(&self) -> bool {
774        // SAFETY: as above.
775        crate::trace_call!("IsThreadpoolTimerSet", self.handle, 0, {
776            // SAFETY: the handle is live until the group releases it.
777            unsafe { IsThreadpoolTimerSet(self.handle) != 0 }
778        })
779    }
780
781    /// Block until all queued and executing callbacks have completed.
782    pub fn wait(&self) {
783        // SAFETY: as above.
784        crate::trace_call!("WaitForThreadpoolTimerCallbacks", self.handle, 0, {
785            // SAFETY: the handle is live until the group releases it.
786            unsafe { WaitForThreadpoolTimerCallbacks(self.handle, FALSE) };
787        });
788    }
789
790    /// Stop the timer and block until no firing is queued or executing.
791    ///
792    /// The same drain [`ThreadpoolTimer::stop_and_drain`] performs, including
793    /// suppressing a re-arm a running callback asks for: without that the drain
794    /// could return with a due time installed, which is the whole reason the
795    /// standalone type has this method rather than only `disarm` and `wait`.
796    ///
797    /// Added because a member that lacked it was not the equivalent of its
798    /// standalone twin that [`CleanupGroup::create_timer`] says it is.
799    pub fn stop_and_drain(&self) {
800        // SAFETY: the group owns this context and does not free it while this
801        // member borrows the group, and `handle` is the object it belongs to.
802        unsafe { ThreadpoolTimer::stop_and_drain_parts(self.context, self.handle) };
803    }
804
805    /// Cancel callbacks that have not started, then wait for those that have.
806    ///
807    /// Unlike the wait member's method of the same name, this carries no
808    /// process-wide hazard. The primitive that can sever a pool's
809    /// arrival-to-factory notification operates on a *wait completion packet*,
810    /// which only a wait owns; a timer has none.
811    pub fn cancel_pending(&self) {
812        // SAFETY: as above.
813        crate::trace_call!("WaitForThreadpoolTimerCallbacks(cancel)", self.handle, 1, {
814            // SAFETY: the handle is live until the group releases it.
815            unsafe { WaitForThreadpoolTimerCallbacks(self.handle, TRUE) };
816        });
817    }
818}
819
820/// A periodic timer owned by a [`CleanupGroup`].
821///
822/// Behaves like [`ThreadpoolPeriodicTimer`] -- including that its ticks may run
823/// concurrently with one another -- but is released by the group rather than by
824/// its own drop.
825#[derive(Debug)]
826pub struct PeriodicTimerMember<'group> {
827    handle: PTP_TIMER,
828    period: Duration,
829    /// The callback context the group owns for this member.
830    ///
831    /// Held so the member can run the same `stop_and_drain` its standalone twin
832    /// does, obligation settlement and all. The group owns and frees it; this
833    /// is a borrow for the member's lifetime.
834    context: *mut c_void,
835    _group: PhantomData<&'group CleanupGroup>,
836}
837
838// SAFETY: the context refers to state the group owns and outlives this member;
839// the member only reads it and passes it to thread-safe pool APIs. The same
840// argument as for `TimerMember`, which holds the same kind of borrow.
841unsafe impl Send for PeriodicTimerMember<'_> {}
842// SAFETY: as above.
843unsafe impl Sync for PeriodicTimerMember<'_> {}
844
845impl PeriodicTimerMember<'_> {
846    /// The period this timer ticks on.
847    #[must_use]
848    pub fn period(&self) -> Duration {
849        self.period
850    }
851
852    /// Start ticking, with the first tick one period from now.
853    pub fn start(&self) {
854        self.start_after(self.period);
855    }
856
857    /// Start ticking, with the first tick `first_delay` from now.
858    pub fn start_after(&self, first_delay: Duration) {
859        // SAFETY: the handle is live until the group releases its members.
860        unsafe {
861            arm_raw(
862                self.handle,
863                relative_filetime(first_delay),
864                millis_u32(self.period),
865                0,
866            );
867        }
868    }
869
870    /// Stop the timer.
871    pub fn stop(&self) {
872        // SAFETY: as above.
873        unsafe { disarm_raw(self.handle) };
874    }
875
876    /// Whether the timer is currently started.
877    #[must_use]
878    pub fn is_running(&self) -> bool {
879        // SAFETY: as above.
880        crate::trace_call!("IsThreadpoolTimerSet", self.handle, 0, {
881            // SAFETY: the handle is live until the group releases it.
882            unsafe { IsThreadpoolTimerSet(self.handle) != 0 }
883        })
884    }
885
886    /// Block until all queued and executing ticks have completed.
887    pub fn wait(&self) {
888        // SAFETY: as above.
889        crate::trace_call!("WaitForThreadpoolTimerCallbacks", self.handle, 0, {
890            // SAFETY: the handle is live until the group releases it.
891            unsafe { WaitForThreadpoolTimerCallbacks(self.handle, FALSE) };
892        });
893    }
894
895    /// Stop the timer and wait until no tick is queued or executing.
896    ///
897    /// The same drain [`ThreadpoolPeriodicTimer::stop_and_drain`] performs,
898    /// including that it holds only provided no other thread starts the member
899    /// during the call: the `start*` methods take `&self`, so a start landing
900    /// between the stop and the drain would leave a schedule installed on
901    /// return.
902    pub fn stop_and_drain(&self) {
903        // SAFETY: the group owns the context and the object and releases both
904        // only when it is dropped, which cannot happen while this borrow lives.
905        unsafe { ThreadpoolPeriodicTimer::stop_and_drain_parts(self.context, self.handle) };
906    }
907}
908
909/// A wait object owned by a [`CleanupGroup`].
910///
911/// Behaves like [`ThreadpoolWait`] but is released by the group rather than by
912/// its own drop, and the watched handle is owned by the group.
913#[derive(Debug)]
914pub struct WaitMember<'group> {
915    handle: PTP_WAIT,
916    watched: *mut WaitTarget,
917    /// The callback context the group owns for this member.
918    ///
919    /// Held so the member can mark its own pool as owing a repair. The group
920    /// owns and frees it; this is a borrow for the member's lifetime.
921    context: *mut c_void,
922    _group: PhantomData<&'group CleanupGroup>,
923}
924
925// SAFETY: both pointers refer to state the group owns and outlives this member;
926// the member only reads them and passes them to thread-safe pool APIs.
927unsafe impl Send for WaitMember<'_> {}
928unsafe impl Sync for WaitMember<'_> {}
929
930/// Every member type is `Send` and `Sync`, and the build is what says so.
931///
932/// A member is a borrow of state the group owns, so these bounds are a property
933/// of the group's ownership rather than of any one member's fields -- which is
934/// exactly why a field can take them away without anything noticing. It
935/// happened: `TimerMember` held only a handle and a `PhantomData` and was
936/// auto-`Send + Sync`; gaining a `*mut c_void` context made it neither, the
937/// `unsafe impl`s that `WaitMember` already carried for the identical reason
938/// were not added beside it, and a public type silently stopped being movable
939/// between threads. No lint, test or sabotage saw it.
940///
941/// So the rule lives here rather than in a test. These functions are never
942/// called; instantiating them is what type-checks the bounds, and a member that
943/// loses one fails the build at the definition rather than in a consumer's
944/// crate. Adding a type to this list is the cost of adding a member kind, and
945/// that is the point.
946///
947/// **Both directions.** A reader may wonder what stops this being vacuous: if
948/// the bounds were dropped from every type at once, would this still pass? No --
949/// `needs_send` and `needs_sync` name the bounds explicitly, so each call is a
950/// real obligation. The accepting direction is checked by the fact that this
951/// compiles at all, and the rejecting direction was checked by measurement: with
952/// `TimerMember`'s impls absent, the first two lines below are the `E0277` that
953/// found this.
954const _: () = {
955    const fn needs_send<T: Send>() {}
956    const fn needs_sync<T: Sync>() {}
957    needs_send::<TimerMember<'_>>();
958    needs_sync::<TimerMember<'_>>();
959    needs_send::<WorkMember<'_>>();
960    needs_sync::<WorkMember<'_>>();
961    needs_send::<PeriodicTimerMember<'_>>();
962    needs_sync::<PeriodicTimerMember<'_>>();
963    needs_send::<WaitMember<'_>>();
964    needs_sync::<WaitMember<'_>>();
965};
966
967impl WaitMember<'_> {
968    /// Borrow the watched handle, for signalling or inspecting it.
969    #[must_use]
970    pub fn handle(&self) -> BorrowedHandle<'_> {
971        // SAFETY: the target is owned by the group, which outlives this member.
972        unsafe { (*self.watched).borrow() }
973    }
974
975    /// Arm the wait, so the next signal or timeout runs the callback once.
976    pub fn arm(&self, timeout: Option<Duration>) {
977        // SAFETY: the object and handle are live until the group releases its
978        // members, which the borrow on `_group` prevents from happening first.
979        unsafe { crate::wait::arm_member(self.handle, &*self.watched, timeout) };
980    }
981
982    /// Stop watching.
983    pub fn disarm(&self) {
984        // SAFETY: as above.
985        unsafe { crate::wait::disarm_raw(self.handle) };
986    }
987
988    /// Block until all queued and executing callbacks have completed.
989    pub fn wait(&self) {
990        // SAFETY: as above.
991        crate::trace_call!("WaitForThreadpoolWaitCallbacks", self.handle, 0, {
992            // SAFETY: the handle is live until the group releases it.
993            unsafe { WaitForThreadpoolWaitCallbacks(self.handle, FALSE) };
994        });
995    }
996
997    /// Stop watching and block until no callback is queued or executing.
998    ///
999    /// The same drain [`ThreadpoolWait::stop_and_drain`] performs, including
1000    /// suppressing a re-arm a running callback asks for: without that the drain
1001    /// could return with the object armed again, which is the whole reason the
1002    /// standalone type has this method rather than only `disarm` and `wait`.
1003    ///
1004    /// Added because a member that lacked it was not the equivalent of its
1005    /// standalone twin that [`CleanupGroup::create_wait`] says it is.
1006    pub fn stop_and_drain(&self) {
1007        // SAFETY: the group owns this context and does not free it while this
1008        // member borrows the group, and `handle` is the object it belongs to.
1009        unsafe { ThreadpoolWait::stop_and_drain_parts(self.context, self.handle) };
1010    }
1011
1012    /// Prefer [`wait`](Self::wait).
1013    ///
1014    /// This crate repairs the pool afterwards -- and on a pool whose repair
1015    /// item could not be created, this call tries to create it rather than
1016    /// giving up. Only if that fails too does the cancellation proceed with
1017    /// nothing to repair it, recording `cancel-untracked` and panicking under
1018    /// `fail-fast`. That is what makes this safe to offer. See
1019    /// [README-FEATURE-self-heal.md](https://docs.rs/crate/windows-threadpool-sys/latest/source/README-FEATURE-self-heal.md).
1020    ///
1021    /// The registry itself is private, so this names it rather than linking it:
1022    /// a public page linking a private item renders a reference the reader
1023    /// cannot follow, which rustdoc warns about.
1024    #[cfg(feature = "self-heal")]
1025    pub fn try_cancel_pending(&self) {
1026        // SAFETY: the obligation this transfers is discharged here, by marking
1027        // the pool so the self-heal repairs it.
1028        unsafe { self.try_cancel_pending_no_heal_tracking() };
1029        // SAFETY: the group owns this context and does not free it while this
1030        // member borrows the group.
1031        let tracked = unsafe { ThreadpoolWait::owe_repair(self.context) };
1032        // Last in the function. Unlike the group-wide release this does not
1033        // free anything, so there is nothing here for an unwind to skip -- but
1034        // the placement matches the other two call sites rather than relying on
1035        // that staying true.
1036        crate::obligation::fail_fast_if_untracked(tracked, "WaitMember");
1037    }
1038
1039    /// `try_cancel_pending` without the repair.
1040    ///
1041    /// Not a link, deliberately: the method it would name does not exist in a
1042    /// build with `self-heal` off, and this one does, so the link would dangle
1043    /// in exactly the configuration this method exists for.
1044    ///
1045    /// **Owning the wait through a cleanup group does not change the hazard.**
1046    /// The group's own `Drop` is safe because it releases with cancel-pending
1047    /// false, not because the group protects its members; a cancelling release
1048    /// passes the cancel through to each one. Named without a link for the
1049    /// reason given above: `close_members_cancelling` does not exist in a build
1050    /// with `self-heal` off, while this method does.
1051    ///
1052    /// # Safety
1053    ///
1054    /// The caller must ensure the pool is repaired, by submitting any work item
1055    /// to it or by knowing something else keeps it live. See
1056    /// [`ThreadpoolWait::try_cancel_pending_no_heal_tracking`], whose obligation
1057    /// this is.
1058    pub unsafe fn try_cancel_pending_no_heal_tracking(&self) {
1059        // SAFETY: as above.
1060        crate::trace_call!("WaitForThreadpoolWaitCallbacks(cancel)", self.handle, 1, {
1061            // SAFETY: the handle is live until the group releases it.
1062            unsafe { WaitForThreadpoolWaitCallbacks(self.handle, TRUE) };
1063        });
1064    }
1065}
1066
1067#[cfg(test)]
1068mod tests;