apple_log/async_api.rs
1//! Executor-agnostic async instrumentation helpers for [`OSActivity`](crate::OSActivity).
2//!
3//! Enable this module with the `async` Cargo feature:
4//!
5//! ```toml
6//! [dependencies]
7//! apple-log = { version = "0.6", features = ["async"] }
8//! ```
9//!
10//! [`ActivityFuture`] wraps any `Future` and enters an `OSActivity` around each
11//! `poll`. This keeps activity scoping aligned with executor wakeups without
12//! holding a scope guard across suspension points.
13//!
14//! # Example
15//!
16//! ```rust,no_run
17//! use apple_log::{OSActivity, OSActivityFlags};
18//!
19//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
20//! let bytes = OSActivity::new(
21//! "download asset",
22//! Some(&OSActivity::current()),
23//! OSActivityFlags::DEFAULT,
24//! )?
25//! .instrument_future(async { 42_usize })
26//! .await?;
27//!
28//! assert_eq!(bytes, 42);
29//! # Ok(())
30//! # }
31//! ```
32
33#![cfg(feature = "async")]
34#![allow(clippy::module_name_repetitions)]
35
36use std::future::Future;
37use std::pin::Pin;
38use std::task::{Context, Poll};
39
40use crate::{LogError, OSActivity};
41
42/// Future wrapper that re-enters an [`OSActivity`] every time the executor polls it.
43#[must_use = "futures do nothing unless awaited or polled"]
44pub struct ActivityFuture<F> {
45 activity: OSActivity,
46 future: F,
47}
48
49impl<F> ActivityFuture<F> {
50 /// Wrap `future` so each poll executes inside `activity`.
51 pub const fn new(activity: OSActivity, future: F) -> Self {
52 Self { activity, future }
53 }
54
55 /// Borrow the activity that will be entered around each poll.
56 #[must_use]
57 pub const fn activity(&self) -> &OSActivity {
58 &self.activity
59 }
60}
61
62impl<F> ActivityFuture<F>
63where
64 F: Future,
65{
66 /// Unwrap the wrapper and return the owned activity plus inner future.
67 #[must_use]
68 pub fn into_parts(self) -> (OSActivity, F) {
69 (self.activity, self.future)
70 }
71}
72
73impl<F> Future for ActivityFuture<F>
74where
75 F: Future,
76{
77 type Output = Result<F::Output, LogError>;
78
79 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
80 // SAFETY: projecting `future` is sound because the outer `ActivityFuture`
81 // is pinned for the duration of this call and we never move `future`
82 // after obtaining the pinned mutable reference.
83 let this = unsafe { self.get_unchecked_mut() };
84 let _scope = match this.activity.enter() {
85 Ok(scope) => scope,
86 Err(error) => return Poll::Ready(Err(error)),
87 };
88
89 // SAFETY: see comment above for the pinned projection rationale.
90 unsafe { Pin::new_unchecked(&mut this.future) }
91 .poll(cx)
92 .map(Ok)
93 }
94}
95
96/// Wrap `future` so each poll executes inside `activity`.
97pub const fn instrument_future<F>(activity: OSActivity, future: F) -> ActivityFuture<F> {
98 ActivityFuture::new(activity, future)
99}