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;