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}