Skip to main content

viceroy_lib/
cache.rs

1use core::str;
2use std::{collections::HashSet, sync::Arc, time::Duration};
3
4use bytes::Bytes;
5#[cfg(test)]
6use proptest_derive::Arbitrary;
7
8use crate::{
9    body::Body,
10    component::bindings::fastly::compute::types::Error as ComponentError,
11    wiggle_abi::types::{BodyHandle, CacheOverrideTag, FastlyStatus},
12};
13
14use http::{HeaderMap, HeaderValue};
15
16mod store;
17mod variance;
18
19use store::{CacheData, CacheKeyObjects, GetBodyBuilder, ObjectMeta, Obligation};
20pub use variance::VaryRule;
21
22#[derive(Debug, thiserror::Error)]
23#[non_exhaustive]
24pub enum Error {
25    #[error("invalid key")]
26    InvalidKey,
27
28    #[error("handle is not writeable")]
29    CannotWrite,
30
31    #[error("no entry for key in cache")]
32    Missing,
33
34    #[error("invalid argument: {0}")]
35    InvalidArgument(&'static str),
36
37    #[error("cache entry's body is currently being read by another body")]
38    HandleBodyUsed,
39
40    #[error("cache entry is not revalidatable")]
41    NotRevalidatable,
42}
43
44impl From<Error> for crate::Error {
45    fn from(value: Error) -> Self {
46        crate::Error::CacheError(value)
47    }
48}
49
50impl From<&Error> for FastlyStatus {
51    fn from(value: &Error) -> Self {
52        match value {
53            // TODO: cceckman-at-fastly: These may not correspond to the same errors as the compute
54            // platform uses. Check!
55            Error::InvalidKey => FastlyStatus::Inval,
56            Error::InvalidArgument(_) => FastlyStatus::Inval,
57            Error::CannotWrite => FastlyStatus::Badf,
58            Error::Missing => FastlyStatus::None,
59            Error::HandleBodyUsed => FastlyStatus::Badf,
60            Error::NotRevalidatable => FastlyStatus::Badf,
61        }
62    }
63}
64
65impl From<Error> for ComponentError {
66    fn from(value: Error) -> Self {
67        match value {
68            // TODO: cceckman-at-fastly: These may not correspond to the same errors as the compute
69            // platform uses. Check!
70            Error::InvalidKey => ComponentError::InvalidArgument,
71            Error::InvalidArgument(_) => ComponentError::InvalidArgument,
72            Error::CannotWrite => ComponentError::AuxiliaryError,
73            Error::Missing => ComponentError::CannotRead,
74            Error::HandleBodyUsed => ComponentError::AuxiliaryError,
75            Error::NotRevalidatable => ComponentError::AuxiliaryError,
76        }
77    }
78}
79
80/// Primary cache key: an up-to-4KiB buffer.
81#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
82#[cfg_attr(test, derive(Arbitrary))]
83pub struct CacheKey(
84    #[cfg_attr(test, proptest(filter = "|f| f.len() <= CacheKey::MAX_LENGTH"))] Vec<u8>,
85);
86
87impl CacheKey {
88    /// The maximum size of a cache key is 4KiB.
89    pub const MAX_LENGTH: usize = 4096;
90}
91
92impl TryFrom<&Vec<u8>> for CacheKey {
93    type Error = Error;
94
95    fn try_from(value: &Vec<u8>) -> Result<Self, Self::Error> {
96        value.as_slice().try_into()
97    }
98}
99
100impl TryFrom<Vec<u8>> for CacheKey {
101    type Error = Error;
102
103    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
104        if value.len() > Self::MAX_LENGTH {
105            Err(Error::InvalidKey)
106        } else {
107            Ok(CacheKey(value))
108        }
109    }
110}
111
112impl TryFrom<&[u8]> for CacheKey {
113    type Error = Error;
114
115    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
116        if value.len() > CacheKey::MAX_LENGTH {
117            Err(Error::InvalidKey)
118        } else {
119            Ok(CacheKey(value.to_owned()))
120        }
121    }
122}
123
124impl TryFrom<&str> for CacheKey {
125    type Error = Error;
126
127    fn try_from(value: &str) -> Result<Self, Self::Error> {
128        value.as_bytes().try_into()
129    }
130}
131
132/// The result of a lookup: the object (if found), and/or an obligation to fetch.
133#[derive(Debug)]
134pub struct CacheEntry {
135    key: CacheKey,
136    found: Option<Found>,
137    go_get: Option<Obligation>,
138
139    /// Respect the range in body() even when the body length is not yet known.
140    ///
141    /// When a cached item is Found, the length of the cached item may or may not be known:
142    /// if no expected length was provided and the body is still streaming, the length is unknown.
143    ///
144    /// When always_use_requested_range is false, and the length is unknown,
145    /// body() returns the full body regardless of the requested range.
146    /// When always_use_requested_range is true, and the length is unknown,
147    /// body() blocks until the start of the range is available.
148    always_use_requested_range: bool,
149}
150
151impl CacheEntry {
152    /// Set the always_use_requested_range flag.
153    /// This applies to all subsequent lookups from this CacheEntry or future entries derived from it.
154    pub fn with_always_use_requested_range(self, always_use_requested_range: bool) -> Self {
155        Self {
156            always_use_requested_range,
157            ..self
158        }
159    }
160
161    /// Return a stub entry to hold in CacheBusy.
162    pub fn stub(&self) -> CacheEntry {
163        Self {
164            key: self.key.clone(),
165            found: None,
166            go_get: None,
167            always_use_requested_range: false,
168        }
169    }
170
171    /// Returns the key used to generate this CacheEntry.
172    pub fn key(&self) -> &CacheKey {
173        &self.key
174    }
175    /// Returns the data found in the cache, if any was present.
176    pub fn found(&self) -> Option<&Found> {
177        self.found.as_ref()
178    }
179
180    /// Returns the data found in the cache, if any was present.
181    pub fn found_mut(&mut self) -> Option<&mut Found> {
182        self.found.as_mut()
183    }
184
185    /// Returns the obligation to fetch, if required
186    pub fn go_get(&self) -> Option<&Obligation> {
187        self.go_get.as_ref()
188    }
189
190    /// Ignore the obligation to fetch, if present.
191    /// Returns true if it actually canceled an obligation.
192    pub fn cancel(&mut self) -> bool {
193        self.go_get.take().is_some()
194    }
195
196    /// Access the body of the cached item, if available.
197    pub async fn body(&self, from: Option<u64>, to: Option<u64>) -> Result<Body, crate::Error> {
198        let found = self
199            .found
200            .as_ref()
201            .ok_or(crate::Error::CacheError(Error::Missing))?;
202        found
203            .get_body()
204            .with_range(from, to)
205            .with_always_use_requested_range(self.always_use_requested_range)
206            .build()
207            .await
208    }
209
210    /// Insert the provided body into the cache.
211    ///
212    /// Returns a CacheEntry where the new item is Found.
213    pub fn insert(
214        &mut self,
215        options: WriteOptions,
216        body: Body,
217    ) -> Result<CacheEntry, crate::Error> {
218        let go_get = self.go_get.take().ok_or(Error::NotRevalidatable)?;
219        let found = go_get.insert(options, body);
220        Ok(CacheEntry {
221            key: self.key.clone(),
222            found: Some(found),
223            go_get: None,
224            always_use_requested_range: self.always_use_requested_range,
225        })
226    }
227
228    /// Freshen the existing cache item according to the new write options,
229    /// without changing the body.
230    pub async fn update(&mut self, options: WriteOptions) -> Result<(), crate::Error> {
231        let go_get = self.go_get.take().ok_or(Error::NotRevalidatable)?;
232        match go_get.update(options).await {
233            Ok(()) => Ok(()),
234            Err((go_get, err)) => {
235                // On failure, preserve the obligation.
236                self.go_get = Some(go_get);
237                Err(err)
238            }
239        }
240    }
241}
242
243/// A successful retrieval of an item from the cache.
244#[derive(Debug)]
245pub struct Found {
246    data: CacheData,
247
248    /// The handle for the last body used to read from this Found.
249    ///
250    /// Only one Body may be outstanding from a given Found at a time.
251    /// (This is an implementation restriction within the compute platform).
252    /// We mirror the BodyHandle here when we create it; we can later check whether the handle is
253    /// still valid, to find an outstanding read.
254    pub last_body_handle: Option<BodyHandle>,
255}
256
257impl From<CacheData> for Found {
258    fn from(data: CacheData) -> Self {
259        Found {
260            data,
261            last_body_handle: None,
262        }
263    }
264}
265
266impl Found {
267    fn get_body(&self) -> GetBodyBuilder<'_> {
268        self.data.body()
269    }
270
271    /// Access the metadata of the cached object.
272    pub fn meta(&self) -> &ObjectMeta {
273        self.data.get_meta()
274    }
275
276    /// The length of the cached object, if known at lookup time.
277    pub fn length(&self) -> Option<u64> {
278        self.data.get_meta().length()
279    }
280}
281
282/// Cache for a service.
283///
284// TODO: cceckman-at-fastly:
285// Explain some about how this works:
286// - Request collapsing
287// - Stale-while-revalidate
288pub struct Cache {
289    inner: moka::future::Cache<CacheKey, Arc<CacheKeyObjects>>,
290}
291
292impl Default for Cache {
293    fn default() -> Self {
294        // TODO: cceckman-at-fastly:
295        // Weight by size, allow a cap on max size?
296        let inner = moka::future::Cache::builder()
297            .eviction_listener(|key, _value, cause| {
298                tracing::info!("cache eviction of {key:?}: {cause:?}")
299            })
300            .build();
301        Cache { inner }
302    }
303}
304
305impl Cache {
306    /// Perform a non-transactional lookup.
307    pub async fn lookup(&self, key: &CacheKey, headers: &HeaderMap) -> CacheEntry {
308        let found = self
309            .inner
310            .get_with_by_ref(key, async { Default::default() })
311            .await
312            .get(headers)
313            .map(|data| Found::from((*data).clone()));
314        CacheEntry {
315            key: key.clone(),
316            found,
317            go_get: None,
318            always_use_requested_range: false,
319        }
320    }
321
322    /// Perform a transactional lookup.
323    pub async fn transaction_lookup(
324        &self,
325        key: &CacheKey,
326        headers: &HeaderMap,
327        ok_to_wait: bool,
328    ) -> CacheEntry {
329        let (found, obligation) = self
330            .inner
331            .get_with_by_ref(key, async { Default::default() })
332            .await
333            .transaction_get(headers, ok_to_wait)
334            .await;
335        CacheEntry {
336            key: key.clone(),
337            found: found.map(|v| Found::from((*v).clone())),
338            go_get: obligation,
339            always_use_requested_range: false,
340        }
341    }
342
343    /// Perform a non-transactional lookup for the given cache key.
344    /// Note: races with other insertions, including transactional insertions.
345    /// Last writer wins!
346    pub async fn insert(
347        &self,
348        key: &CacheKey,
349        request_headers: HeaderMap,
350        options: WriteOptions,
351        body: Body,
352    ) {
353        self.inner
354            .get_with_by_ref(key, async { Default::default() })
355            .await
356            .insert(request_headers, options, body, None);
357    }
358
359    /// Purge/soft-purge all cache entries corresponding to the given surrogate key.
360    /// Returns the number of entries (variants) purged.
361    ///
362    /// Note: this does not block concurrent reads _or inserts_; an insertion can race with the
363    /// purge.
364    pub fn purge(&self, key: SurrogateKey, soft_purge: bool) -> usize {
365        self.inner
366            .iter()
367            .map(|(_, entry)| entry.purge(&key, soft_purge))
368            .sum()
369    }
370}
371
372/// Options that can be applied to a write, e.g. insert or transaction_insert.
373#[derive(Default, Clone)]
374pub struct WriteOptions {
375    pub max_age: Duration,
376    pub initial_age: Duration,
377    pub stale_while_revalidate: Duration,
378    pub vary_rule: VaryRule,
379    pub user_metadata: Bytes,
380    pub length: Option<u64>,
381    pub sensitive_data: bool,
382    pub edge_max_age: Duration,
383    pub surrogate_keys: SurrogateKeySet,
384}
385
386impl WriteOptions {
387    pub fn new(max_age: Duration) -> Self {
388        WriteOptions {
389            max_age,
390            initial_age: Duration::ZERO,
391            stale_while_revalidate: Duration::ZERO,
392            vary_rule: VaryRule::default(),
393            user_metadata: Bytes::new(),
394            length: None,
395            sensitive_data: false,
396            edge_max_age: max_age,
397            surrogate_keys: Default::default(),
398        }
399    }
400}
401
402/// Optional override for response caching behavior.
403#[derive(Clone, Debug, Default)]
404pub enum CacheOverride {
405    /// Do not override the behavior specified in the origin response's cache control headers.
406    #[default]
407    None,
408    /// Do not cache the response to this request, regardless of the origin response's headers.
409    Pass,
410    /// Override particular cache control settings.
411    ///
412    /// The origin response's cache control headers will be used for ttl and stale_while_revalidate if `None`.
413    Override {
414        ttl: Option<u32>,
415        stale_while_revalidate: Option<u32>,
416        pci: bool,
417        surrogate_key: Option<HeaderValue>,
418    },
419}
420
421impl CacheOverride {
422    pub fn is_pass(&self) -> bool {
423        matches!(self, Self::Pass)
424    }
425
426    /// Convert from the representation suitable for passing across the ABI boundary.
427    ///
428    /// Returns `None` if the tag is not recognized. Depending on the tag, some of the values may be
429    /// ignored.
430    pub fn from_abi(
431        tag: u32,
432        ttl: u32,
433        swr: u32,
434        surrogate_key: Option<HeaderValue>,
435    ) -> Option<Self> {
436        CacheOverrideTag::from_bits(tag).map(|tag| {
437            if tag.contains(CacheOverrideTag::PASS) {
438                return CacheOverride::Pass;
439            }
440            if tag.is_empty() && surrogate_key.is_none() {
441                return CacheOverride::None;
442            }
443            let ttl = if tag.contains(CacheOverrideTag::TTL) {
444                Some(ttl)
445            } else {
446                None
447            };
448            let stale_while_revalidate = if tag.contains(CacheOverrideTag::STALE_WHILE_REVALIDATE) {
449                Some(swr)
450            } else {
451                None
452            };
453            let pci = tag.contains(CacheOverrideTag::PCI);
454            CacheOverride::Override {
455                ttl,
456                stale_while_revalidate,
457                pci,
458                surrogate_key,
459            }
460        })
461    }
462}
463
464/// Maximum length of surrogate keys (when combined with spaces).
465const MAX_SURROGATE_KEYS_LENGTH: usize = 16 * 1024;
466/// Maximum length of a single surrogate key.
467const MAX_SURROGATE_KEY_LENGTH: usize = 1024;
468
469#[derive(Debug, Default, Clone)]
470pub struct SurrogateKeySet(HashSet<SurrogateKey>);
471
472impl std::fmt::Display for SurrogateKeySet {
473    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
474        write!(f, "{{")?;
475        for (i, item) in self.0.iter().enumerate() {
476            if i == 0 {
477                write!(f, "{item}")?;
478            } else {
479                write!(f, " {item}")?;
480            }
481        }
482        write!(f, "}}")
483    }
484}
485
486impl TryFrom<&[u8]> for SurrogateKeySet {
487    type Error = crate::Error;
488
489    fn try_from(s: &[u8]) -> Result<Self, Self::Error> {
490        if s.len() > MAX_SURROGATE_KEYS_LENGTH {
491            return Err(
492                Error::InvalidArgument("surrogate key set exceeds maximum length (16Ki)").into(),
493            );
494        }
495        let result: Result<HashSet<_>, _> = s
496            .split(|c| *c == b' ')
497            .filter(|sk| !sk.is_empty())
498            .map(SurrogateKey::try_from)
499            .collect();
500        Ok(SurrogateKeySet(result?))
501    }
502}
503
504impl std::str::FromStr for SurrogateKeySet {
505    type Err = crate::Error;
506
507    fn from_str(s: &str) -> Result<Self, Self::Err> {
508        s.as_bytes().try_into()
509    }
510}
511
512/// A validated surrogate key: a non-empty string containing only visible ASCII characters.
513#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone)]
514pub struct SurrogateKey(String);
515
516impl std::fmt::Display for SurrogateKey {
517    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
518        self.0.fmt(f)
519    }
520}
521
522impl TryFrom<&[u8]> for SurrogateKey {
523    type Error = Error;
524
525    fn try_from(b: &[u8]) -> Result<Self, Self::Error> {
526        if b.len() > MAX_SURROGATE_KEY_LENGTH {
527            return Err(Error::InvalidArgument(
528                "surrogate key exceeds maximum length (1024)",
529            ));
530        }
531
532        if !b.iter().all(|c| c.is_ascii_graphic()) {
533            return Err(Error::InvalidArgument(
534                "surrogate key contains characters other than graphical ASCII",
535            ));
536        }
537        let s = unsafe {
538            // All graphic ASCII characters are equivalent to the corresponding UTF-8 codepoints.
539            str::from_utf8_unchecked(b)
540        };
541        Ok(SurrogateKey(s.to_owned()))
542    }
543}
544
545impl std::str::FromStr for SurrogateKey {
546    type Err = Error;
547
548    fn from_str(s: &str) -> Result<Self, Self::Err> {
549        s.as_bytes().try_into()
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use std::rc::Rc;
556
557    use http::HeaderName;
558    use proptest::prelude::*;
559
560    use super::*;
561
562    proptest! {
563        #[test]
564        fn reject_cache_key_too_long(l in 4097usize..5000) {
565            let mut v : Vec<u8> = Vec::new();
566            v.resize(l, 0);
567            CacheKey::try_from(&v).unwrap_err();
568        }
569    }
570
571    proptest! {
572        #[test]
573        fn accept_valid_cache_key_len(l in 0usize..4096) {
574            let mut v : Vec<u8> = Vec::new();
575            v.resize(l, 0);
576            let _ = CacheKey::try_from(&v).unwrap();
577        }
578    }
579
580    proptest! {
581        #[test]
582        fn nontransactional_insert_lookup(
583                key in any::<CacheKey>(),
584                max_age in any::<u32>(),
585                initial_age in any::<u32>(),
586                value in any::<Vec<u8>>()) {
587            let cache = Cache::default();
588
589            // We can't use tokio::test and proptest! together; both alter the signature of the
590            // test function, and are not aware of each other enough for it to pass.
591            let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
592            rt.block_on(async {
593                let empty = cache.lookup(&key, &HeaderMap::default()).await;
594                assert!(empty.found().is_none());
595                // The non-transactional case does not produce an obligation (go_get)
596                assert!(empty.go_get().is_none());
597
598                let write_options = WriteOptions {
599                    max_age: Duration::from_secs(max_age as u64),
600                    initial_age: Duration::from_secs(initial_age as u64),
601                    ..Default::default()
602                };
603
604                cache.insert(&key, HeaderMap::default(), write_options, value.clone().into()).await;
605
606                let nonempty = cache.lookup(&key, &HeaderMap::default()).await;
607                let found = nonempty.found().expect("should have found inserted key");
608                let got = found.get_body().build().await.unwrap().read_into_vec().await.unwrap();
609                assert_eq!(got, value);
610            });
611        }
612    }
613
614    #[test]
615    fn parse_surrogate_keys() {
616        let keys: SurrogateKeySet = "keyA keyB".parse().unwrap();
617        let key_a: SurrogateKey = "keyA".parse().unwrap();
618        let key_b: SurrogateKey = "keyB".parse().unwrap();
619        assert_eq!(keys.0.len(), 2);
620        assert!(keys.0.contains(&key_a));
621        assert!(keys.0.contains(&key_b));
622    }
623
624    #[tokio::test]
625    async fn insert_immediately_stale() {
626        let cache = Cache::default();
627        let key = ([1u8].as_slice()).try_into().unwrap();
628
629        // Insert an already-stale entry:
630        let write_options = WriteOptions {
631            max_age: Duration::from_secs(1),
632            initial_age: Duration::from_secs(2),
633            ..Default::default()
634        };
635
636        let mut body = Body::empty();
637        body.push_back([1u8].as_slice());
638
639        cache
640            .insert(&key, HeaderMap::default(), write_options, body)
641            .await;
642
643        let nonempty = cache.lookup(&key, &HeaderMap::default()).await;
644        let found = nonempty.found().expect("should have found inserted key");
645        assert!(!found.meta().is_fresh());
646    }
647
648    #[tokio::test]
649    async fn test_vary() {
650        let cache = Cache::default();
651        let key = ([1u8].as_slice()).try_into().unwrap();
652
653        let header_name = HeaderName::from_static("x-viceroy-test");
654        let request_headers: HeaderMap = [(header_name.clone(), HeaderValue::from_static("test"))]
655            .into_iter()
656            .collect();
657
658        let write_options = WriteOptions {
659            max_age: Duration::from_secs(100),
660            vary_rule: VaryRule::new([&header_name].into_iter()),
661            ..Default::default()
662        };
663        let body = Body::empty();
664        cache
665            .insert(&key, request_headers.clone(), write_options, body)
666            .await;
667
668        let empty_headers = cache.lookup(&key, &HeaderMap::default()).await;
669        assert!(empty_headers.found().is_none());
670
671        let matched_headers = cache.lookup(&key, &request_headers).await;
672        assert!(matched_headers.found.is_some());
673
674        let r2_headers: HeaderMap = [(header_name.clone(), HeaderValue::from_static("assert"))]
675            .into_iter()
676            .collect();
677        let mismatched_headers = cache.lookup(&key, &r2_headers).await;
678        assert!(mismatched_headers.found.is_none());
679    }
680
681    #[tokio::test]
682    async fn insert_stale_while_revalidate() {
683        let cache = Cache::default();
684        let key = ([1u8].as_slice()).try_into().unwrap();
685
686        // Insert an already-stale entry, with an SWR period:
687        let write_options = WriteOptions {
688            max_age: Duration::from_secs(1),
689            initial_age: Duration::from_secs(2),
690            stale_while_revalidate: Duration::from_secs(10),
691            ..Default::default()
692        };
693
694        let mut body = Body::empty();
695        body.push_back([1u8].as_slice());
696        cache
697            .insert(&key, HeaderMap::default(), write_options, body)
698            .await;
699
700        let nonempty = cache.lookup(&key, &HeaderMap::default()).await;
701        let found = nonempty.found().expect("should have found inserted key");
702        assert!(!found.meta().is_fresh());
703        assert!(found.meta().is_usable());
704    }
705
706    #[tokio::test]
707    async fn insert_and_revalidate() {
708        let cache = Cache::default();
709        let key = ([1u8].as_slice()).try_into().unwrap();
710
711        // Insert an already-stale entry, with an SWR period:
712        let write_options = WriteOptions {
713            max_age: Duration::from_secs(1),
714            initial_age: Duration::from_secs(2),
715            stale_while_revalidate: Duration::from_secs(10),
716            ..Default::default()
717        };
718
719        let mut body = Body::empty();
720        body.push_back([1u8].as_slice());
721        cache
722            .insert(&key, HeaderMap::default(), write_options, body)
723            .await;
724
725        let mut txn1 = cache
726            .transaction_lookup(&key, &HeaderMap::default(), true)
727            .await;
728        let found = txn1.found().expect("should have found inserted key");
729        assert!(!found.meta().is_fresh());
730        assert!(found.meta().is_usable());
731
732        assert!(txn1.go_get().is_some());
733
734        // Another lookup should not get the obligation *or* block
735        {
736            let txn2 = cache
737                .transaction_lookup(&key, &HeaderMap::default(), true)
738                .await;
739            let found = txn2.found().expect("should have found inserted key");
740            assert!(!found.meta().is_fresh());
741            assert!(found.meta().is_usable());
742            assert!(txn2.go_get().is_none());
743        }
744
745        txn1.update(WriteOptions {
746            max_age: Duration::from_secs(10),
747            stale_while_revalidate: Duration::from_secs(10),
748            ..WriteOptions::default()
749        })
750        .await
751        .unwrap();
752
753        // After this, should get the new response:
754        let txn3 = cache
755            .transaction_lookup(&key, &HeaderMap::default(), true)
756            .await;
757        assert!(txn3.go_get().is_none());
758        let found = txn3.found().expect("should find updated entry");
759        assert!(found.meta().is_usable());
760        assert!(found.meta().is_fresh());
761    }
762
763    #[tokio::test]
764    async fn cannot_revalidate_first_insert() {
765        let cache = Cache::default();
766        let key = ([1u8].as_slice()).try_into().unwrap();
767
768        let mut txn1 = cache
769            .transaction_lookup(&key, &HeaderMap::default(), false)
770            .await;
771        assert!(txn1.found().is_none());
772        let opts = WriteOptions {
773            max_age: Duration::from_secs(10),
774            stale_while_revalidate: Duration::from_secs(10),
775            ..WriteOptions::default()
776        };
777        txn1.update(opts.clone()).await.unwrap_err();
778
779        // But we should still be able to insert.
780        txn1.insert(opts.clone(), Body::empty()).unwrap();
781    }
782
783    #[tokio::test]
784    async fn purge_by_surrogate_key() {
785        let cache = Cache::default();
786        let key: CacheKey = ([1u8].as_slice()).try_into().unwrap();
787
788        let surrogate_keys: SurrogateKeySet = "one two three".parse().unwrap();
789        let opts = WriteOptions {
790            max_age: Duration::from_secs(100),
791            surrogate_keys: surrogate_keys.clone(),
792            ..WriteOptions::default()
793        };
794
795        cache
796            .insert(&key, HeaderMap::default(), opts.clone(), Body::empty())
797            .await;
798        let result = cache.lookup(&key, &HeaderMap::default()).await;
799        assert!(result.found().is_some());
800
801        assert_eq!(cache.purge("two".parse().unwrap(), false), 1);
802        let result = cache.lookup(&key, &HeaderMap::default()).await;
803        assert!(result.found().is_none());
804    }
805
806    #[tokio::test]
807    async fn purge_variant_by_surrogate_key() {
808        let cache = Rc::new(Cache::default());
809        let key: CacheKey = ([1u8].as_slice()).try_into().unwrap();
810
811        // "Introduction and Variations on a Theme by Mozart, Op. 9, is one of Fernando Sor's most
812        // famous works for guitar."
813        let vary_header = HeaderName::from_static("opus-9");
814        let insert = {
815            let cache = cache.clone();
816            let key = key.clone();
817
818            move |surrogate_keys: &str, variation: &str| {
819                let mut headers = HeaderMap::new();
820                headers.insert(vary_header.clone(), variation.try_into().unwrap());
821                let surrogate_keys: SurrogateKeySet = surrogate_keys.parse().unwrap();
822                let opts = WriteOptions {
823                    max_age: Duration::from_secs(100),
824                    surrogate_keys: surrogate_keys.clone(),
825                    vary_rule: VaryRule::new([&vary_header]),
826                    ..WriteOptions::default()
827                };
828                let cache = cache.clone();
829                let key = key.clone();
830                async move {
831                    cache
832                        .insert(&key, headers.clone(), opts.clone(), Body::empty())
833                        .await;
834                    headers
835                }
836            }
837        };
838
839        let h1 = insert("one two three", "twinkle twinkle").await;
840        let h2 = insert("one three", "abcdefg").await;
841        assert!(cache.lookup(&key, &h1).await.found().is_some());
842        assert!(cache.lookup(&key, &h2).await.found().is_some());
843
844        assert_eq!(cache.purge("two".parse().unwrap(), false), 1);
845        assert!(cache.lookup(&key, &h1).await.found().is_none());
846        assert!(cache.lookup(&key, &h2).await.found().is_some());
847    }
848
849    #[tokio::test]
850    async fn soft_purge() {
851        let cache = Cache::default();
852        let key: CacheKey = ([1u8].as_slice()).try_into().unwrap();
853
854        let surrogate_keys: SurrogateKeySet = "one two three".parse().unwrap();
855        let opts = WriteOptions {
856            max_age: Duration::from_secs(100),
857            surrogate_keys: surrogate_keys.clone(),
858            ..WriteOptions::default()
859        };
860
861        cache
862            .insert(&key, HeaderMap::default(), opts.clone(), Body::empty())
863            .await;
864        let result = cache.lookup(&key, &HeaderMap::default()).await;
865        assert!(result.found().is_some());
866
867        assert_eq!(cache.purge("two".parse().unwrap(), true), 1);
868        let result = cache
869            .transaction_lookup(&key, &HeaderMap::default(), false)
870            .await;
871        let found = result.found().unwrap();
872        assert!(!found.meta().is_fresh());
873        assert!(found.meta().is_usable());
874        assert!(result.go_get().is_some());
875    }
876}