Skip to main content

hitbox_tower/
layer.rs

1//! Tower layer and builder for HTTP caching.
2//!
3//! This module provides [`Cache`], a Tower [`Layer`] that wraps services with
4//! caching behavior, and [`CacheBuilder`] for fluent configuration.
5//!
6//! # Examples
7//!
8//! ```
9//! use std::time::Duration;
10//! use hitbox::Config;
11//! use hitbox::policy::PolicyConfig;
12//! use hitbox_tower::Cache;
13//! use hitbox_moka::MokaBackend;
14//! use hitbox_http::extractors::Method;
15//! use hitbox_http::predicates::{NeutralRequestPredicate, NeutralResponsePredicate};
16//!
17//! # use http_body_util::Full;
18//! # type Body = Full<bytes::Bytes>;
19//! let config = Config::builder()
20//!     .request_predicate(NeutralRequestPredicate::new())
21//!     .response_predicate(NeutralResponsePredicate::new())
22//!     .extractor(Method::new())
23//!     .policy(PolicyConfig::builder().ttl(Duration::from_secs(60)).build())
24//!     .build();
25//! # let _: Config<
26//! #     hitbox_http::predicates::NeutralRequestPredicate<Body>,
27//! #     hitbox_http::predicates::NeutralResponsePredicate<Body>,
28//! #     Method<hitbox_http::extractors::NeutralExtractor<Body>>,
29//! # > = config;
30//!
31//! let cache_layer = Cache::builder()
32//!     .backend(MokaBackend::builder().max_entries(1000).build())
33//!     .config(config)
34//!     .build();
35//! ```
36//!
37//! [`Layer`]: tower::Layer
38
39use std::sync::Arc;
40
41use hitbox::backend::CacheBackend;
42use hitbox::concurrency::NoopConcurrencyManager;
43use hitbox_core::DisabledOffload;
44use hitbox_http::DEFAULT_CACHE_STATUS_HEADER;
45use http::header::HeaderName;
46use tower::Layer;
47
48use crate::service::CacheService;
49
50/// Marker type for unset builder fields.
51pub struct NotSet;
52
53/// Tower [`Layer`] that adds HTTP caching to a service.
54///
55/// `Cache` wraps any Tower service with caching behavior. When a request arrives,
56/// the layer evaluates predicates to determine cacheability, generates a cache key
57/// using extractors, and either returns a cached response or forwards to the
58/// upstream service.
59///
60/// # Type Parameters
61///
62/// * `B` - Cache backend (e.g., [`MokaBackend`], `RedisBackend`). Must implement
63///   [`CacheBackend`].
64/// * `C` - Configuration providing predicates, extractors, and policy. Use
65///   [`hitbox::Config`] to build custom configuration.
66/// * `CM` - Concurrency manager for dogpile prevention. Use [`NoopConcurrencyManager`]
67///   to disable or [`BroadcastConcurrencyManager`] to enable.
68/// * `O` - Offload strategy for background revalidation. Use [`DisabledOffload`]
69///   for synchronous behavior.
70///
71/// # Examples
72///
73/// Create with the builder pattern:
74///
75/// ```
76/// use hitbox_tower::Cache;
77/// use hitbox_moka::MokaBackend;
78/// use hitbox::Config;
79/// use hitbox_http::extractors::Method;
80/// use hitbox_http::predicates::{NeutralRequestPredicate, NeutralResponsePredicate};
81/// # use http_body_util::Full;
82/// # type Body = Full<bytes::Bytes>;
83///
84/// let config = Config::builder()
85///     .request_predicate(NeutralRequestPredicate::new())
86///     .response_predicate(NeutralResponsePredicate::new())
87///     .extractor(Method::new())
88///     .build();
89/// # let _: Config<
90/// #     NeutralRequestPredicate<Body>,
91/// #     NeutralResponsePredicate<Body>,
92/// #     Method<hitbox_http::extractors::NeutralExtractor<Body>>,
93/// # > = config;
94///
95/// let cache_layer = Cache::builder()
96///     .backend(MokaBackend::builder().max_entries(1000).build())
97///     .config(config)
98///     .build();
99/// ```
100///
101/// [`Layer`]: tower::Layer
102/// [`MokaBackend`]: hitbox_moka::MokaBackend
103/// [`CacheBackend`]: hitbox::backend::CacheBackend
104/// [`NoopConcurrencyManager`]: hitbox::concurrency::NoopConcurrencyManager
105/// [`BroadcastConcurrencyManager`]: hitbox::concurrency::BroadcastConcurrencyManager
106/// [`DisabledOffload`]: hitbox_core::DisabledOffload
107#[derive(Clone)]
108pub struct Cache<B, C, CM, O = DisabledOffload> {
109    /// The cache backend for storing and retrieving responses.
110    pub backend: Arc<B>,
111    /// Configuration with predicates, extractors, and cache policy.
112    pub configuration: C,
113    /// Offload strategy for background tasks.
114    pub offload: O,
115    /// Concurrency manager for dogpile prevention.
116    pub concurrency_manager: CM,
117    /// Header name for cache status (HIT/MISS/STALE).
118    pub cache_status_header: HeaderName,
119}
120
121impl<S, B, C, CM, O> Layer<S> for Cache<B, C, CM, O>
122where
123    C: Clone,
124    CM: Clone,
125    O: Clone,
126{
127    type Service = CacheService<S, B, C, CM, O>;
128
129    fn layer(&self, upstream: S) -> Self::Service {
130        CacheService::new(
131            upstream,
132            Arc::clone(&self.backend),
133            self.configuration.clone(),
134            self.offload.clone(),
135            self.concurrency_manager.clone(),
136            self.cache_status_header.clone(),
137        )
138    }
139}
140
141impl Cache<NotSet, NotSet, NoopConcurrencyManager, DisabledOffload> {
142    /// Creates a new [`CacheBuilder`].
143    ///
144    /// Both [`backend()`](CacheBuilder::backend) and [`config()`](CacheBuilder::config)
145    /// must be called before [`build()`](CacheBuilder::build).
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// use hitbox_tower::Cache;
151    /// use hitbox_moka::MokaBackend;
152    /// use hitbox::Config;
153    /// use hitbox_http::extractors::Method;
154    /// use hitbox_http::predicates::{NeutralRequestPredicate, NeutralResponsePredicate};
155    /// # use http_body_util::Full;
156    /// # type Body = Full<bytes::Bytes>;
157    ///
158    /// let config = Config::builder()
159    ///     .request_predicate(NeutralRequestPredicate::new())
160    ///     .response_predicate(NeutralResponsePredicate::new())
161    ///     .extractor(Method::new())
162    ///     .build();
163    /// # let _: Config<
164    /// #     NeutralRequestPredicate<Body>,
165    /// #     NeutralResponsePredicate<Body>,
166    /// #     Method<hitbox_http::extractors::NeutralExtractor<Body>>,
167    /// # > = config;
168    ///
169    /// let cache_layer = Cache::builder()
170    ///     .backend(MokaBackend::builder().max_entries(1000).build())
171    ///     .config(config)
172    ///     .build();
173    /// ```
174    pub fn builder() -> CacheBuilder<NotSet, NotSet, NoopConcurrencyManager, DisabledOffload> {
175        CacheBuilder::new()
176    }
177}
178
179/// Fluent builder for constructing a [`Cache`] layer.
180///
181/// Use [`Cache::builder()`] to create a new builder. Both [`backend()`](Self::backend)
182/// and [`config()`](Self::config) must be called before [`build()`](Self::build).
183///
184/// # Type Parameters
185///
186/// The type parameters change as you call builder methods:
187///
188/// * `B` - Backend type, set by [`backend()`](Self::backend)
189/// * `C` - Configuration type, set by [`config()`](Self::config)
190/// * `CM` - Concurrency manager type, set by [`concurrency_manager()`](Self::concurrency_manager)
191/// * `O` - Offload type, set by [`offload()`](Self::offload)
192///
193/// # Examples
194///
195/// ```
196/// use std::time::Duration;
197/// use hitbox_tower::Cache;
198/// use hitbox_moka::MokaBackend;
199/// use hitbox::Config;
200/// use hitbox::policy::PolicyConfig;
201/// use hitbox_http::extractors::Method;
202/// use hitbox_http::predicates::{NeutralRequestPredicate, NeutralResponsePredicate};
203/// use http::header::HeaderName;
204/// # use http_body_util::Full;
205/// # type Body = Full<bytes::Bytes>;
206///
207/// let config = Config::builder()
208///     .request_predicate(NeutralRequestPredicate::new())
209///     .response_predicate(NeutralResponsePredicate::new())
210///     .extractor(Method::new())
211///     .policy(PolicyConfig::builder().ttl(Duration::from_secs(300)).build())
212///     .build();
213/// # let _: Config<
214/// #     NeutralRequestPredicate<Body>,
215/// #     NeutralResponsePredicate<Body>,
216/// #     Method<hitbox_http::extractors::NeutralExtractor<Body>>,
217/// # > = config;
218///
219/// let layer = Cache::builder()
220///     .backend(MokaBackend::builder().max_entries(10_000).build())
221///     .config(config)
222///     .cache_status_header(HeaderName::from_static("x-custom-cache"))
223///     .build();
224/// ```
225pub struct CacheBuilder<B, C, CM, O = DisabledOffload> {
226    backend: B,
227    configuration: C,
228    offload: O,
229    concurrency_manager: CM,
230    cache_status_header: Option<HeaderName>,
231}
232
233impl CacheBuilder<NotSet, NotSet, NoopConcurrencyManager, DisabledOffload> {
234    /// Creates a new builder.
235    ///
236    /// Prefer using [`Cache::builder()`] instead of calling this directly.
237    pub fn new() -> Self {
238        Self {
239            backend: NotSet,
240            configuration: NotSet,
241            offload: DisabledOffload,
242            concurrency_manager: NoopConcurrencyManager,
243            cache_status_header: None,
244        }
245    }
246}
247
248impl Default for CacheBuilder<NotSet, NotSet, NoopConcurrencyManager, DisabledOffload> {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254impl<B, C, CM, O> CacheBuilder<B, C, CM, O> {
255    /// Sets the cache backend for storing responses.
256    ///
257    /// Common backends:
258    ///
259    /// - `MokaBackend` — In-memory cache (from `hitbox-moka`)
260    /// - `RedisBackend` — Distributed cache via Redis (from `hitbox-redis`)
261    ///
262    /// # Examples
263    ///
264    /// ```
265    /// use hitbox_tower::Cache;
266    /// use hitbox_moka::MokaBackend;
267    ///
268    /// let builder = Cache::builder()
269    ///     .backend(MokaBackend::builder().max_entries(1000).build());
270    /// ```
271    pub fn backend<NB: CacheBackend>(self, backend: NB) -> CacheBuilder<NB, C, CM, O> {
272        CacheBuilder {
273            backend,
274            configuration: self.configuration,
275            offload: self.offload,
276            concurrency_manager: self.concurrency_manager,
277            cache_status_header: self.cache_status_header,
278        }
279    }
280
281    /// Sets the cache configuration with predicates, extractors, and policy.
282    ///
283    /// Use [`Config::builder()`](hitbox::Config::builder) to create a configuration with:
284    /// - Request predicates (which requests to cache)
285    /// - Response predicates (which responses to cache)
286    /// - Extractors (how to generate cache keys)
287    /// - Policy (TTL, stale handling)
288    ///
289    /// # Examples
290    ///
291    /// ```
292    /// use std::time::Duration;
293    /// use hitbox_tower::Cache;
294    /// use hitbox_moka::MokaBackend;
295    /// use hitbox::Config;
296    /// use hitbox::policy::PolicyConfig;
297    /// use hitbox_http::extractors::Method;
298    /// use hitbox_http::predicates::{NeutralRequestPredicate, NeutralResponsePredicate};
299    /// # use http_body_util::Full;
300    /// # type Body = Full<bytes::Bytes>;
301    ///
302    /// let config = Config::builder()
303    ///     .request_predicate(NeutralRequestPredicate::new())
304    ///     .response_predicate(NeutralResponsePredicate::new())
305    ///     .extractor(Method::new())
306    ///     .policy(PolicyConfig::builder().ttl(Duration::from_secs(60)).build())
307    ///     .build();
308    /// # let _: Config<
309    /// #     NeutralRequestPredicate<Body>,
310    /// #     NeutralResponsePredicate<Body>,
311    /// #     Method<hitbox_http::extractors::NeutralExtractor<Body>>,
312    /// # > = config;
313    ///
314    /// let layer = Cache::builder()
315    ///     .backend(MokaBackend::builder().max_entries(1000).build())
316    ///     .config(config)
317    ///     .build();
318    /// ```
319    pub fn config<NC>(self, configuration: NC) -> CacheBuilder<B, NC, CM, O> {
320        CacheBuilder {
321            backend: self.backend,
322            configuration,
323            offload: self.offload,
324            concurrency_manager: self.concurrency_manager,
325            cache_status_header: self.cache_status_header,
326        }
327    }
328
329    /// Sets the concurrency manager for dogpile prevention.
330    ///
331    /// The dogpile effect occurs when a cache entry expires and multiple
332    /// concurrent requests all try to refresh it simultaneously. A concurrency
333    /// manager prevents this by coordinating requests.
334    ///
335    /// Options:
336    /// - [`NoopConcurrencyManager`] — No coordination (default)
337    /// - [`BroadcastConcurrencyManager`] — One request fetches, others wait
338    ///
339    /// [`NoopConcurrencyManager`]: hitbox::concurrency::NoopConcurrencyManager
340    /// [`BroadcastConcurrencyManager`]: hitbox::concurrency::BroadcastConcurrencyManager
341    pub fn concurrency_manager<NCM>(self, concurrency_manager: NCM) -> CacheBuilder<B, C, NCM, O> {
342        CacheBuilder {
343            backend: self.backend,
344            configuration: self.configuration,
345            offload: self.offload,
346            concurrency_manager,
347            cache_status_header: self.cache_status_header,
348        }
349    }
350
351    /// Sets the offload strategy for background revalidation.
352    ///
353    /// When serving stale content, the offload strategy determines how
354    /// background refresh is performed.
355    ///
356    /// Defaults to [`DisabledOffload`] (synchronous revalidation).
357    ///
358    /// [`DisabledOffload`]: hitbox_core::DisabledOffload
359    pub fn offload<NO>(self, offload: NO) -> CacheBuilder<B, C, CM, NO> {
360        CacheBuilder {
361            backend: self.backend,
362            configuration: self.configuration,
363            offload,
364            concurrency_manager: self.concurrency_manager,
365            cache_status_header: self.cache_status_header,
366        }
367    }
368
369    /// Sets the header name for cache status.
370    ///
371    /// The cache status header indicates whether a response was served from cache.
372    /// Possible values are `HIT`, `MISS`, or `STALE`.
373    ///
374    /// Defaults to [`DEFAULT_CACHE_STATUS_HEADER`] (`x-cache-status`).
375    ///
376    /// # Examples
377    ///
378    /// ```
379    /// use hitbox_tower::Cache;
380    /// use hitbox_moka::MokaBackend;
381    /// use http::header::HeaderName;
382    ///
383    /// let builder = Cache::builder()
384    ///     .backend(MokaBackend::builder().max_entries(1000).build())
385    ///     .cache_status_header(HeaderName::from_static("x-custom-cache"));
386    /// ```
387    pub fn cache_status_header(self, header_name: HeaderName) -> Self {
388        CacheBuilder {
389            cache_status_header: Some(header_name),
390            ..self
391        }
392    }
393}
394
395impl<B, C, CM, O> CacheBuilder<B, C, CM, O>
396where
397    B: CacheBackend,
398{
399    /// Builds the [`Cache`] layer.
400    ///
401    /// Both [`backend()`](Self::backend) and [`config()`](Self::config) must
402    /// be called before this method.
403    pub fn build(self) -> Cache<B, C, CM, O> {
404        Cache {
405            backend: Arc::new(self.backend),
406            configuration: self.configuration,
407            offload: self.offload,
408            concurrency_manager: self.concurrency_manager,
409            cache_status_header: self
410                .cache_status_header
411                .unwrap_or(DEFAULT_CACHE_STATUS_HEADER),
412        }
413    }
414}