Skip to main content

hitbox_core/
offload.rs

1//! Offload trait for background task execution.
2//!
3//! This module provides the [`Offload`] trait which abstracts over
4//! different implementations for spawning background tasks.
5//!
6//! # Lifetime Parameter
7//!
8//! The `Offload<'a>` trait is parameterized by a lifetime to support both:
9//! - `'static` futures (for real background execution with `OffloadManager`)
10//! - Non-`'static` futures (for middleware integration with `DisabledOffload`)
11//!
12//! This design allows `CacheFuture` to work with borrowed upstreams (like reqwest
13//! middleware's `Next<'_>`) when background revalidation is not needed.
14
15use std::future::Future;
16use std::hash::Hash;
17
18use smol_str::SmolStr;
19
20use crate::CacheKey;
21
22/// Key for identifying offloaded tasks.
23///
24/// This enum represents different types of keys that can be used to identify
25/// background tasks.
26///
27/// # Variants
28///
29/// - [`Keyed`](OffloadKey::Keyed): Key derived from a cache key.
30/// - [`Explicit`](OffloadKey::Explicit): Key with explicit id provided by caller.
31/// - [`Auto`](OffloadKey::Auto): Key with auto-assigned id (manager assigns).
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33pub enum OffloadKey {
34    /// Key derived from cache key.
35    Keyed {
36        /// The cache key.
37        key: CacheKey,
38        /// Kind of the task (e.g., "revalidate", "cache_write").
39        kind: SmolStr,
40    },
41    /// Key with explicit id provided by caller.
42    Explicit {
43        /// Kind of the task (e.g., "cleanup", "metrics").
44        kind: SmolStr,
45        /// Unique identifier within the kind.
46        id: u64,
47    },
48    /// Key with auto-assigned id (manager assigns internally).
49    Auto {
50        /// Kind of the task (e.g., "race_loser", "background").
51        kind: SmolStr,
52    },
53}
54
55impl OffloadKey {
56    /// Create a keyed offload key derived from a cache key.
57    ///
58    /// # Example
59    ///
60    /// ```
61    /// use hitbox_core::{CacheKey, OffloadKey};
62    ///
63    /// let cache_key = CacheKey::from_str("user", "123");
64    /// let key = OffloadKey::keyed(cache_key, "revalidate");
65    /// ```
66    pub fn keyed(key: CacheKey, kind: impl Into<SmolStr>) -> Self {
67        Self::Keyed {
68            key,
69            kind: kind.into(),
70        }
71    }
72
73    /// Create a key with explicit id provided by caller.
74    ///
75    /// # Example
76    ///
77    /// ```
78    /// use hitbox_core::OffloadKey;
79    ///
80    /// let key = OffloadKey::explicit("cleanup", 42);
81    /// ```
82    pub fn explicit(kind: impl Into<SmolStr>, id: u64) -> Self {
83        Self::Explicit {
84            kind: kind.into(),
85            id,
86        }
87    }
88
89    /// Create an auto key where manager assigns id internally.
90    ///
91    /// # Example
92    ///
93    /// ```
94    /// use hitbox_core::OffloadKey;
95    ///
96    /// let key = OffloadKey::auto("race_loser");
97    /// ```
98    pub fn auto(kind: impl Into<SmolStr>) -> Self {
99        Self::Auto { kind: kind.into() }
100    }
101
102    /// Returns the kind of this key.
103    ///
104    /// Used for metrics labels and tracing.
105    pub fn kind(&self) -> &SmolStr {
106        match self {
107            Self::Keyed { kind, .. } => kind,
108            Self::Explicit { kind, .. } => kind,
109            Self::Auto { kind } => kind,
110        }
111    }
112}
113
114/// Conversion from `(CacheKey, S)` tuple to `OffloadKey::Keyed`.
115///
116/// # Example
117///
118/// ```
119/// use hitbox_core::{CacheKey, OffloadKey};
120///
121/// let cache_key = CacheKey::from_str("user", "123");
122/// let offload_key: OffloadKey = (cache_key, "revalidate").into();
123/// ```
124impl<S: Into<SmolStr>> From<(CacheKey, S)> for OffloadKey {
125    fn from((key, kind): (CacheKey, S)) -> Self {
126        Self::keyed(key, kind)
127    }
128}
129
130/// Trait for spawning background tasks.
131///
132/// This trait allows components like `CacheFuture` and `CompositionBackend`
133/// to offload work to be executed in the background without blocking the main
134/// request path.
135///
136/// # Lifetime Parameter
137///
138/// The lifetime parameter `'a` determines what futures can be spawned:
139/// - `Offload<'static>`: Can spawn futures that live forever (real background tasks)
140/// - `Offload<'a>`: Can only spawn futures that live at least as long as `'a`
141///
142/// This enables [`DisabledOffload`] to accept any lifetime (since it doesn't
143/// actually spawn anything), while `OffloadManager` requires `'static`.
144///
145/// # Implementations
146///
147/// - [`DisabledOffload`]: Does nothing, accepts any lifetime. Use when background
148///   execution is not needed (e.g., reqwest middleware integration).
149/// - `OffloadManager` (in `hitbox` crate): Real background execution, requires `'static`.
150///
151/// # Clone bound
152///
153/// Implementors should use `Arc` internally to ensure all cloned instances
154/// share the same configuration and state.
155///
156/// # Example
157///
158/// ```ignore
159/// use hitbox_core::{Offload, OffloadKey};
160///
161/// fn offload_cache_write<'a, O: Offload<'a>>(offload: &O, key: CacheKey) {
162///     offload.register((key, "cache_write"), async move {
163///         // Perform background cache write
164///         println!("Writing to cache");
165///     });
166/// }
167/// ```
168pub trait Offload<'a>: Send + Sync + Clone {
169    /// Spawn a future to be executed in the background.
170    ///
171    /// The future will be executed asynchronously and its result will be
172    /// handled according to the implementation's policy.
173    ///
174    /// # Arguments
175    ///
176    /// * `kind` - A label categorizing the task type (e.g., "revalidate", "cache_write").
177    ///   Used for metrics and tracing.
178    /// * `future` - The future to execute in the background. Must be `Send + 'a`.
179    ///   For real background execution, `'a` must be `'static`.
180    ///
181    /// # Deprecation
182    ///
183    /// This method will be removed in version 0.3. Use [`register`](Self::register) instead:
184    ///
185    /// ```ignore
186    /// // Before (deprecated)
187    /// offload.spawn("revalidate", async { /* ... */ });
188    ///
189    /// // After
190    /// offload.register(OffloadKey::generated("revalidate", id), async { /* ... */ });
191    /// ```
192    #[deprecated(
193        since = "0.2.1",
194        note = "use `register` instead, will be removed in 0.3"
195    )]
196    fn spawn<F>(&self, kind: impl Into<SmolStr>, future: F)
197    where
198        F: Future<Output = ()> + Send + 'a;
199
200    /// Register a future to be executed in the background.
201    ///
202    /// This is the primary method for spawning background tasks.
203    ///
204    /// # Arguments
205    ///
206    /// * `key` - The key identifying this task. A tuple `(CacheKey, &str)` can also be passed.
207    /// * `future` - The future to execute in the background. Must be `Send + 'a`.
208    ///
209    /// # Example
210    ///
211    /// ```ignore
212    /// use hitbox_core::{Offload, OffloadKey, CacheKey};
213    ///
214    /// let cache_key = CacheKey::from_str("user", "123");
215    /// offload.register((cache_key, "revalidate"), async {
216    ///     // Revalidate cache entry
217    /// });
218    ///
219    /// offload.register(OffloadKey::explicit("cleanup", 1), async {
220    ///     // Cleanup task
221    /// });
222    /// ```
223    fn register<K, F>(&self, key: K, future: F)
224    where
225        K: Into<OffloadKey>,
226        F: Future<Output = ()> + Send + 'a,
227    {
228        let key = key.into();
229        #[allow(deprecated)]
230        self.spawn(key.kind().clone(), future)
231    }
232}
233
234/// A disabled offload implementation that discards all spawned tasks.
235///
236/// This implementation accepts futures with any lifetime since it doesn't
237/// actually execute them. Use this when:
238/// - Background revalidation is not needed
239/// - Integrating with middleware systems that have non-`'static` types
240///   (e.g., reqwest middleware's `Next<'_>`)
241///
242/// # Example
243///
244/// ```
245/// use hitbox_core::{Offload, DisabledOffload, OffloadKey};
246///
247/// let offload = DisabledOffload;
248///
249/// // This works even with non-'static futures
250/// let borrowed_data = String::from("hello");
251/// let borrowed_ref = &borrowed_data;
252/// offload.register(OffloadKey::explicit("test", 0), async move {
253///     // Would use borrowed_ref here
254///     let _ = borrowed_ref;
255/// });
256/// ```
257#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
258pub struct DisabledOffload;
259
260impl<'a> Offload<'a> for DisabledOffload {
261    #[inline]
262    #[allow(deprecated)]
263    fn spawn<F>(&self, _kind: impl Into<SmolStr>, _future: F)
264    where
265        F: Future<Output = ()> + Send + 'a,
266    {
267        // Intentionally does nothing.
268        // The future is dropped without execution.
269    }
270
271    #[inline]
272    fn register<K, F>(&self, _key: K, _future: F)
273    where
274        K: Into<OffloadKey>,
275        F: Future<Output = ()> + Send + 'a,
276    {
277        // Intentionally does nothing.
278        // The future is dropped without execution.
279    }
280}