Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4556ead
Remove some completed TODOs
cceckman-at-fastly Mar 26, 2025
6567b35
Remove some unnecessary structure in CacheValue
cceckman-at-fastly Mar 26, 2025
2dd1806
Switch to watch::Sender for response-keyed objects
cceckman-at-fastly Mar 26, 2025
4abb7a9
Move request_headers out of write_options
cceckman-at-fastly Mar 27, 2025
9e5f8b0
Inner parts of transactional API: get_or_obligate
cceckman-at-fastly Mar 26, 2025
c9f10cd
Orthogonalize presence and obligation
cceckman-at-fastly Mar 27, 2025
4982999
Transactional insert, part 1
cceckman-at-fastly Mar 27, 2025
3cc21c8
Basic test for obligation
cceckman-at-fastly Mar 27, 2025
0775f23
Transactional insert
cceckman-at-fastly Mar 27, 2025
3cc6710
Add more tests
cceckman-at-fastly Mar 27, 2025
a464113
Transactional lookup in ABI
cceckman-at-fastly Mar 28, 2025
efec647
Transactional insert wiring to ABI
cceckman-at-fastly Mar 28, 2025
b3a0b6a
Additional tests, hostcalls for transactional API
cceckman-at-fastly Mar 31, 2025
ef5d38c
Migrate to new error type
cceckman-at-fastly Mar 31, 2025
481ce8c
Finish implementing cancel
cceckman-at-fastly Apr 22, 2025
506504b
Revert logging change (was meant to be printf debugging)
cceckman-at-fastly Apr 24, 2025
95e2ad9
Include a missing optimization: skip lock on explicit complete
cceckman-at-fastly Apr 24, 2025
83a287c
Rename test variables for legibility
cceckman-at-fastly Apr 24, 2025
479c664
Ensure `transaction_lookup_async` polls once before returning
cceckman-at-fastly Apr 30, 2025
cf37f76
Merge branch 'main' into cceckman/cache-transact
cceckman-at-fastly Apr 30, 2025
54d0a1f
Add comment on `awaitable` variable
cceckman-at-fastly May 5, 2025
2bf2139
Reorder blocks and add comment on write lock notifications
cceckman-at-fastly May 5, 2025
525a1f9
Add more comments
cceckman-at-fastly May 5, 2025
2cdc0d6
Use std::mem::take instead of ::swap
cceckman-at-fastly May 5, 2025
04d9b0e
Add test for collapsing across vary rules
cceckman-at-fastly May 5, 2025
fc09643
Freshen position of used vary rule at insertion
cceckman-at-fastly May 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion cli/tests/integration/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use {
hyper::StatusCode,
};

viceroy_test!(cache_request_works, |is_component| {
viceroy_test!(cache_nontransactional, |is_component| {
if !std::env::var("ENABLE_EXPERIMENTAL_CACHE_API").is_ok_and(|v| v == "1") {
eprintln!("WARNING: Skipping cache tests.");
eprintln!(
Expand All @@ -22,3 +22,20 @@ viceroy_test!(cache_request_works, |is_component| {
assert_eq!(resp.status(), StatusCode::OK);
Ok(())
});

viceroy_test!(cache_transactional, |is_component| {
if !std::env::var("ENABLE_EXPERIMENTAL_CACHE_API").is_ok_and(|v| v == "1") {
eprintln!("WARNING: Skipping cache tests.");
eprintln!(
"Set ENABLE_EXPERIMENTAL_CACHE_API=1 to enable experimental cache API and run tests."
);
return Ok(());
}

let resp = Test::using_fixture("cache_transactional.wasm")
.adapt_component(is_component)
.against_empty()
.await?;
assert_eq!(resp.status(), StatusCode::OK);
Ok(())
});
156 changes: 131 additions & 25 deletions lib/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,66 @@ use proptest_derive::Arbitrary;

use crate::{
body::Body,
wiggle_abi::types::{BodyHandle, CacheOverrideTag},
Error,
component::fastly::api::types::Error as ComponentError,
wiggle_abi::types::{BodyHandle, CacheOverrideTag, FastlyStatus},
};
use fastly_shared::FastlyStatus;

use http::{HeaderMap, HeaderValue};

mod store;
mod variance;

use store::{CacheData, CacheKeyObjects, ObjectMeta};
use store::{CacheData, CacheKeyObjects, ObjectMeta, Obligation};
pub use variance::VaryRule;

#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("invalid key")]
InvalidKey,

#[error("handle is not writeable")]
CannotWrite,

#[error("no entry for key in cache")]
Missing,

#[error("cache entry's body is currently being read by another body")]
HandleBodyUsed,
}

impl From<Error> for crate::Error {
fn from(value: Error) -> Self {
crate::Error::CacheError(value)
}
}

impl From<&Error> for FastlyStatus {
fn from(value: &Error) -> Self {
match value {
// TODO: cceckman-at-fastly: These may not correspond to the same errors as the compute
// platform uses. Check!
Error::InvalidKey => FastlyStatus::Inval,
Error::CannotWrite => FastlyStatus::Badf,
Error::Missing => FastlyStatus::None,
Error::HandleBodyUsed => FastlyStatus::Badf,
}
}
}

impl From<Error> for ComponentError {
fn from(value: Error) -> Self {
match value {
// TODO: cceckman-at-fastly: These may not correspond to the same errors as the compute
// platform uses. Check!
Error::InvalidKey => ComponentError::InvalidArgument,
Error::CannotWrite => ComponentError::BadHandle,
Error::Missing => ComponentError::OptionalNone,
Error::HandleBodyUsed => ComponentError::BadHandle,
}
}
}

/// Primary cache key: an up-to-4KiB buffer.
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(test, derive(Arbitrary))]
Expand All @@ -30,54 +78,63 @@ impl CacheKey {
}

impl TryFrom<&Vec<u8>> for CacheKey {
type Error = FastlyStatus;
type Error = Error;

fn try_from(value: &Vec<u8>) -> Result<Self, Self::Error> {
value.as_slice().try_into()
}
}

impl TryFrom<Vec<u8>> for CacheKey {
type Error = FastlyStatus;
type Error = Error;

fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
if value.len() > Self::MAX_LENGTH {
Err(FastlyStatus::BUFLEN)
Err(Error::InvalidKey)
} else {
Ok(CacheKey(value))
}
}
}

impl TryFrom<&[u8]> for CacheKey {
type Error = FastlyStatus;
type Error = Error;

fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
if value.len() > CacheKey::MAX_LENGTH {
Err(FastlyStatus::BUFLEN)
Err(Error::InvalidKey)
} else {
Ok(CacheKey(value.to_owned()))
}
}
}

impl TryFrom<&str> for CacheKey {
type Error = FastlyStatus;
type Error = Error;

fn try_from(value: &str) -> Result<Self, Self::Error> {
value.as_bytes().try_into()
}
}

/// The result of a lookup: the object (if found), or an obligation to get it (if not).
/// The result of a lookup: the object (if found), and/or an obligation to fetch.
#[derive(Debug)]
pub struct CacheEntry {
key: CacheKey,
found: Option<Found>,
// TODO: cceckman-at-fastly 2025-02-26: GoGet
go_get: Option<Obligation>,
}

impl CacheEntry {
/// Return a stub entry to hold in CacheBusy.
pub fn stub(&self) -> CacheEntry {
Self {
key: self.key.clone(),
found: None,
go_get: None,
}
}

/// Returns the key used to generate this CacheEntry.
pub fn key(&self) -> &CacheKey {
&self.key
Expand All @@ -91,6 +148,16 @@ impl CacheEntry {
pub fn found_mut(&mut self) -> Option<&mut Found> {
self.found.as_mut()
}

/// Returns the obligation to fetch, if required
pub fn go_get(&self) -> Option<&Obligation> {
self.go_get.as_ref()
}

/// Extract the write obligation, if present.
pub fn take_go_get(&mut self) -> Option<Obligation> {
self.go_get.take()
}
}

/// A successful retrieval of an item from the cache.
Expand All @@ -109,7 +176,7 @@ pub struct Found {

impl Found {
/// Access the body of the cached object.
pub fn body(&self) -> Result<Body, Error> {
pub fn body(&self) -> Result<Body, crate::Error> {
self.data.as_ref().get_body()
}

Expand All @@ -123,7 +190,6 @@ impl Found {
///
// TODO: cceckman-at-fastly:
// Explain some about how this works:
// - Request-keyed vs. response-keyed items; variance
// - Request collapsing
// - Stale-while-revalidate
pub struct Cache {
Expand All @@ -144,7 +210,7 @@ impl Default for Cache {
}

impl Cache {
/// Perform a non-transactional lookup for the given cache key.
/// Perform a non-transactional lookup.
pub async fn lookup(&self, key: &CacheKey, headers: &HeaderMap) -> CacheEntry {
let found = self
.inner
Expand All @@ -158,29 +224,68 @@ impl Cache {
CacheEntry {
key: key.clone(),
found,
go_get: None,
}
}

/// Perform a transactional lookup.
pub async fn transaction_lookup(
&self,
key: &CacheKey,
headers: &HeaderMap,
ok_to_wait: bool,
) -> CacheEntry {
let (found, obligation) = self
.inner
.get_with_by_ref(&key, async { Default::default() })
.await
.transaction_get(headers, ok_to_wait)
.await;
CacheEntry {
key: key.clone(),
found: found.map(|data| Found {
data,
last_body_handle: None,
}),
go_get: obligation,
}
}

/// Perform a non-transactional lookup for the given cache key.
/// Note: races with other insertions, including transactional insertions.
/// Last writer wins!
pub async fn insert(&self, key: &CacheKey, options: WriteOptions, body: Body) {
pub async fn insert(
&self,
key: &CacheKey,
request_headers: HeaderMap,
options: WriteOptions,
body: Body,
) {
self.inner
.get_with_by_ref(&key, async { Default::default() })
.await
.insert(options, body);
.insert(request_headers, options, body, None);
}
}

/// Options that can be applied to a write, e.g. insert or transaction_insert.
#[derive(Default)]
pub struct WriteOptions {
pub max_age: Duration,
pub initial_age: Duration,

pub request_headers: HeaderMap,
pub vary_rule: VaryRule,
}

impl WriteOptions {
pub fn new(max_age: Duration) -> Self {
WriteOptions {
max_age,
initial_age: Duration::ZERO,
vary_rule: VaryRule::default(),
}
}
}

/// Optional override for response caching behavior.
#[derive(Clone, Debug, Default)]
pub enum CacheOverride {
Expand Down Expand Up @@ -292,11 +397,10 @@ mod tests {
let write_options = WriteOptions {
max_age: Duration::from_secs(max_age as u64),
initial_age: Duration::from_secs(initial_age as u64),
request_headers: HeaderMap::default(),
vary_rule: VaryRule::default(),
};

cache.insert(&key, write_options, value.clone().into()).await;
cache.insert(&key, HeaderMap::default(), write_options, value.clone().into()).await;

let nonempty = cache.lookup(&key, &HeaderMap::default()).await;
let found = nonempty.found().expect("should have found inserted key");
Expand All @@ -315,14 +419,15 @@ mod tests {
let write_options = WriteOptions {
max_age: Duration::from_secs(1),
initial_age: Duration::from_secs(2),
request_headers: HeaderMap::default(),
vary_rule: VaryRule::default(),
};

let mut body = Body::empty();
body.push_back([1u8].as_slice());

cache.insert(&key, write_options, body).await;
cache
.insert(&key, HeaderMap::default(), write_options, body)
.await;

let nonempty = cache.lookup(&key, &HeaderMap::default()).await;
let found = nonempty.found().expect("should have found inserted key");
Expand All @@ -342,11 +447,12 @@ mod tests {
let write_options = WriteOptions {
max_age: Duration::from_secs(100),
initial_age: Duration::from_secs(2),
request_headers: request_headers.clone(),
vary_rule: VaryRule::new([&header_name].into_iter()),
};
let body = Body::empty();
cache.insert(&key, write_options, body).await;
cache
.insert(&key, request_headers.clone(), write_options, body)
.await;

let empty_headers = cache.lookup(&key, &HeaderMap::default()).await;
assert!(empty_headers.found().is_none());
Expand Down
Loading
Loading