diff --git a/crates/adapter/src/fastly/cache.rs b/crates/adapter/src/fastly/cache.rs index 0d4b8912..91ac67b1 100644 --- a/crates/adapter/src/fastly/cache.rs +++ b/crates/adapter/src/fastly/cache.rs @@ -3,6 +3,7 @@ use crate::{alloc_result_opt, TrappingUnwrap}; pub type BusyHandle = u32; pub type CacheHandle = u32; +pub type ReplaceHandle = u32; pub type CacheObjectLength = u64; pub type CacheDurationNs = u64; @@ -51,9 +52,26 @@ bitflags::bitflags! { const LENGTH = 1 << 6; const USER_METADATA = 1 << 7; const SENSITIVE_DATA = 1 << 8; + const EDGE_MAX_AGE_NS = 1 << 9; + } +} + +bitflags::bitflags! { + #[repr(transparent)] + pub struct CacheReplaceOptionsMask: u32 { + const _RESERVED = 1 << 0; + const REQUEST_HEADERS = 1 << 1; + const REPLACE_STRATEGY = 1 << 2; } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct CacheReplaceOptions { + pub request_headers: RequestHandle, + pub replace_strategy: u32, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(C)] pub struct CacheGetBodyOptions { @@ -104,6 +122,21 @@ mod cache { } } + impl From for cache::ReplaceOptionsMask { + fn from(value: CacheReplaceOptionsMask) -> Self { + let mut flags = Self::empty(); + flags.set( + Self::REQUEST_HEADERS, + value.contains(CacheReplaceOptionsMask::REQUEST_HEADERS), + ); + flags.set( + Self::REPLACE_STRATEGY, + value.contains(CacheReplaceOptionsMask::REPLACE_STRATEGY), + ); + flags + } + } + impl From for cache::WriteOptionsMask { fn from(value: CacheWriteOptionsMask) -> Self { let mut flags = Self::empty(); @@ -140,6 +173,10 @@ mod cache { Self::SENSITIVE_DATA, value.contains(CacheWriteOptionsMask::SENSITIVE_DATA), ); + flags.set( + Self::EDGE_MAX_AGE_NS, + value.contains(CacheWriteOptionsMask::EDGE_MAX_AGE_NS), + ); flags } } @@ -232,6 +269,196 @@ mod cache { } } + #[export_name = "fastly_cache#replace"] + fn replace( + cache_key_ptr: *const u8, + cache_key_len: usize, + options_mask: CacheReplaceOptionsMask, + options: *const CacheReplaceOptions, + replace_handle_out: *mut ReplaceHandle, + ) -> FastlyStatus { + let cache_key = unsafe { slice::from_raw_parts(cache_key_ptr, cache_key_len) }; + let options_mask = cache::ReplaceOptionsMask::from(options_mask); + + let options = unsafe { + cache::ReplaceOptions { + request_headers: (*options).request_headers, + replace_strategy: (*options).replace_strategy, + } + }; + + let res = cache::replace(cache_key, options_mask, options); + + match res { + Ok(res) => { + unsafe { + *replace_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#replace_get_age_ns"] + fn replace_get_age_ns( + handle: ReplaceHandle, + duration_out: *mut CacheDurationNs, + ) -> FastlyStatus { + match cache::replace_get_age_ns(handle) { + Ok(res) => { + unsafe { + *duration_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#replace_get_body"] + fn replace_get_body( + handle: ReplaceHandle, + options_mask: CacheGetBodyOptionsMask, + options: *const CacheGetBodyOptions, + body_handle_out: *mut BodyHandle, + ) -> FastlyStatus { + let options_mask = cache::GetBodyOptionsMask::from(options_mask); + let options = unsafe { cache::GetBodyOptions::from(*options) }; + match cache::replace_get_body(handle, options_mask, options) { + Ok(res) => { + unsafe { + *body_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#replace_get_hits"] + fn replace_get_hits(handle: ReplaceHandle, hits_out: *mut CacheHitCount) -> FastlyStatus { + match cache::replace_get_hits(handle) { + Ok(res) => { + unsafe { + *hits_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#replace_get_length"] + fn replace_get_length( + handle: ReplaceHandle, + length_out: *mut CacheObjectLength, + ) -> FastlyStatus { + match cache::replace_get_length(handle) { + Ok(res) => { + unsafe { + *length_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#replace_get_max_age_ns"] + fn replace_get_max_age_ns( + handle: ReplaceHandle, + duration_out: *mut CacheDurationNs, + ) -> FastlyStatus { + match cache::replace_get_max_age_ns(handle) { + Ok(res) => { + unsafe { + *duration_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#replace_get_stale_while_revalidate_ns"] + fn replace_get_stale_while_revalidate_ns( + handle: ReplaceHandle, + duration_out: *mut CacheDurationNs, + ) -> FastlyStatus { + match cache::replace_get_stale_while_revalidate_ns(handle) { + Ok(res) => { + unsafe { + *duration_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#replace_get_state"] + fn replace_get_state( + handle: ReplaceHandle, + cache_lookup_state_out: *mut CacheLookupState, + ) -> FastlyStatus { + match cache::replace_get_state(handle) { + Ok(res) => { + unsafe { + *cache_lookup_state_out = res.into(); + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_cache#replace_get_user_metadata"] + fn replace_get_user_metadata( + handle: ReplaceHandle, + user_metadata_out_ptr: *mut u8, + user_metadata_out_len: usize, + nwritten_out: *mut usize, + ) -> FastlyStatus { + alloc_result_opt!( + user_metadata_out_ptr, + user_metadata_out_len, + nwritten_out, + { + cache::replace_get_user_metadata( + handle, + u64::try_from(user_metadata_out_len).trapping_unwrap(), + ) + } + ) + } + + #[export_name = "fastly_cache#replace_insert"] + fn replace_insert( + handle: ReplaceHandle, + options_mask: CacheWriteOptionsMask, + options: *const CacheWriteOptions, + body_handle_out: *mut BodyHandle, + ) -> FastlyStatus { + let options_mask = cache::WriteOptionsMask::from(options_mask); + + let options = unsafe { write_options(options) }; + + let res = cache::replace_insert(handle, options_mask, &options); + + std::mem::forget(options); + + match res { + Ok(res) => { + unsafe { + *body_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + #[export_name = "fastly_cache#transaction_lookup"] pub fn transaction_lookup( cache_key_ptr: *const u8, diff --git a/lib/compute-at-edge-abi/cache.witx b/lib/compute-at-edge-abi/cache.witx index 5b202fd7..08930d92 100644 --- a/lib/compute-at-edge-abi/cache.witx +++ b/lib/compute-at-edge-abi/cache.witx @@ -2,9 +2,13 @@ (typename $cache_handle (handle)) ;;; Handle that can be used to check whether or not a cache lookup is waiting on another client. (typename $cache_busy_handle (handle)) +;;; Handle for an in-progress Replace operation +(typename $cache_replace_handle (handle)) + (typename $cache_object_length u64) (typename $cache_duration_ns u64) (typename $cache_hit_count u64) +(typename $cache_replace_strategy u32) ;;; Extensible options for cache lookup operations; currently used for both `lookup` and `transaction_lookup`. (typename $cache_lookup_options @@ -20,6 +24,22 @@ ) ) +;;; Extensible options for cache replace operations +(typename $cache_replace_options + (record + (field $request_headers $request_handle) ;; a full request handle, but used only for its headers + (field $replace_strategy $cache_replace_strategy) + ) +) + +(typename $cache_replace_options_mask + (flags (@witx repr u32) + $reserved + $request_headers + $replace_strategy + ) +) + ;;; Configuration for several hostcalls that write to the cache: ;;; - `insert` ;;; - `transaction_insert` @@ -44,6 +64,7 @@ (field $length $cache_object_length) (field $user_metadata_ptr (@witx pointer u8)) (field $user_metadata_len (@witx usize)) + (field $edge_max_age_ns $cache_duration_ns) ) ) @@ -58,6 +79,7 @@ $length $user_metadata $sensitive_data + $edge_max_age_ns ) ) @@ -108,6 +130,93 @@ (result $err (expected $body_handle (error $fastly_status))) ) + ;;; The entrypoint to the replace API. + ;;; + ;;; This operation always participates in request collapsing and may return stale objects. + (@interface func (export "replace") + (param $cache_key (list u8)) + (param $options_mask $cache_replace_options_mask) + (param $options (@witx pointer $cache_replace_options)) + (result $err (expected $cache_replace_handle (error $fastly_status))) + ) + + ;;; Replace an object in the cache with the given metadata + ;;; + ;;; The returned handle is to a streaming body that is used for writing the object into + ;;; the cache. + (@interface func (export "replace_insert") + (param $handle $cache_replace_handle) + (param $options_mask $cache_write_options_mask) + (param $options (@witx pointer $cache_write_options)) + (result $err (expected $body_handle (error $fastly_status))) + ) + + ;;; Gets the age of the existing object during replace, returning the + ;;; `$none` error if there was no object. + (@interface func (export "replace_get_age_ns") + (param $handle $cache_replace_handle) + (result $err (expected $cache_duration_ns (error $fastly_status))) + ) + + ;;; Gets a range of the existing object body, returning the `$none` error if there + ;;; was no existing object. + ;;; + ;;; The returned `body_handle` must be closed before calling this function + ;;; again on the same `cache_replace_handle`. + (@interface func (export "replace_get_body") + (param $handle $cache_replace_handle) + (param $options_mask $cache_get_body_options_mask) + (param $options $cache_get_body_options) + (result $err (expected $body_handle (error $fastly_status))) + ) + + ;;; Gets the number of cache hits for the existing object during replace, + ;;; returning the `$none` error if there was no object. + (@interface func (export "replace_get_hits") + (param $handle $cache_replace_handle) + (result $err (expected $cache_hit_count (error $fastly_status))) + ) + + ;;; Gets the content length of the existing object during replace, + ;;; returning the `$none` error if there was no object, or no content + ;;; length was provided. + (@interface func (export "replace_get_length") + (param $handle $cache_replace_handle) + (result $err (expected $cache_object_length (error $fastly_status))) + ) + + ;;; Gets the configured max age of the existing object during replace, + ;;; returning the `$none` error if there was no object. + (@interface func (export "replace_get_max_age_ns") + (param $handle $cache_replace_handle) + (result $err (expected $cache_duration_ns (error $fastly_status))) + ) + + ;;; Gets the configured stale-while-revalidate period of the existing + ;;; object during replace, returning the `$none` error if there was no + ;;; object. + (@interface func (export "replace_get_stale_while_revalidate_ns") + (param $handle $cache_replace_handle) + (result $err (expected $cache_duration_ns (error $fastly_status))) + ) + + ;;; Gets the lookup state of the existing object during replace, returning + ;;; the `$none` error if there was no object. + (@interface func (export "replace_get_state") + (param $handle $cache_replace_handle) + (result $err (expected $cache_lookup_state (error $fastly_status))) + ) + + ;;; Gets the user metadata of the existing object during replace, returning + ;;; the `$none` error if there was no object. + (@interface func (export "replace_get_user_metadata") + (param $handle $cache_replace_handle) + (param $user_metadata_out_ptr (@witx pointer u8)) + (param $user_metadata_out_len (@witx usize)) + (param $nwritten_out (@witx pointer (@witx usize))) + (result $err (expected (error $fastly_status))) + ) + ;;; The entrypoint to the request-collapsing cache transaction API. ;;; ;;; This operation always participates in request collapsing and may return stale objects. To bypass @@ -222,10 +331,6 @@ ;;; ;;; The returned `body_handle` must be closed before calling this function again on the same ;;; `cache_handle`. - ;;; - ;;; Note: until the CacheD protocol is adjusted to fully support this functionality, - ;;; the body of objects that are past the stale-while-revalidate period will not - ;;; be available, even when other metadata is. (@interface func (export "get_body") (param $handle $cache_handle) (param $options_mask $cache_get_body_options_mask) diff --git a/lib/data/viceroy-component-adapter.wasm b/lib/data/viceroy-component-adapter.wasm index 657aad70..ed93e5bb 100755 Binary files a/lib/data/viceroy-component-adapter.wasm and b/lib/data/viceroy-component-adapter.wasm differ diff --git a/lib/src/component/cache.rs b/lib/src/component/cache.rs index 6222c32d..51cfd739 100644 --- a/lib/src/component/cache.rs +++ b/lib/src/component/cache.rs @@ -29,6 +29,113 @@ impl cache::Host for ComponentCtx { .into()) } + async fn replace( + &mut self, + _key: String, + _options_mask: cache::ReplaceOptionsMask, + _options: cache::ReplaceOptions, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn replace_get_age_ns( + &mut self, + _handle: cache::ReplaceHandle, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn replace_get_body( + &mut self, + _handle: cache::ReplaceHandle, + _options_mask: cache::GetBodyOptionsMask, + _options: cache::GetBodyOptions, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn replace_get_hits( + &mut self, + _handle: cache::ReplaceHandle, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn replace_get_length( + &mut self, + _handle: cache::ReplaceHandle, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn replace_get_max_age_ns( + &mut self, + _handle: cache::ReplaceHandle, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn replace_get_stale_while_revalidate_ns( + &mut self, + _handle: cache::ReplaceHandle, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn replace_get_state( + &mut self, + _handle: cache::ReplaceHandle, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn replace_get_user_metadata( + &mut self, + _handle: cache::ReplaceHandle, + _max_len: u64, + ) -> Result>, types::Error> { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn replace_insert( + &mut self, + _handle: cache::ReplaceHandle, + _options_mask: cache::WriteOptionsMask, + _options: cache::WriteOptions, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + async fn get_body( &mut self, _handle: cache::Handle, diff --git a/lib/src/wiggle_abi/cache.rs b/lib/src/wiggle_abi/cache.rs index b1482d27..6db603af 100644 --- a/lib/src/wiggle_abi/cache.rs +++ b/lib/src/wiggle_abi/cache.rs @@ -25,6 +25,95 @@ impl FastlyCache for Session { Err(Error::NotAvailable("Cache API primitives")) } + fn replace( + &mut self, + memory: &mut wiggle::GuestMemory<'_>, + cache_key: wiggle::GuestPtr<[u8]>, + options_mask: types::CacheReplaceOptionsMask, + abi_options: wiggle::GuestPtr, + ) -> Result { + Err(Error::NotAvailable("Cache API primitives")) + } + + fn replace_get_age_ns( + &mut self, + memory: &mut wiggle::GuestMemory<'_>, + cache_handle: types::CacheReplaceHandle, + ) -> Result { + Err(Error::NotAvailable("Cache API primitives")) + } + + fn replace_get_body( + &mut self, + memory: &mut wiggle::GuestMemory<'_>, + cache_handle: types::CacheReplaceHandle, + options_mask: types::CacheGetBodyOptionsMask, + options: &types::CacheGetBodyOptions, + ) -> Result { + Err(Error::NotAvailable("Cache API primitives")) + } + + fn replace_get_hits( + &mut self, + memory: &mut wiggle::GuestMemory<'_>, + cache_handle: types::CacheReplaceHandle, + ) -> Result { + Err(Error::NotAvailable("Cache API primitives")) + } + + fn replace_get_length( + &mut self, + memory: &mut wiggle::GuestMemory<'_>, + cache_handle: types::CacheReplaceHandle, + ) -> Result { + Err(Error::NotAvailable("Cache API primitives")) + } + + fn replace_get_max_age_ns( + &mut self, + memory: &mut wiggle::GuestMemory<'_>, + cache_handle: types::CacheReplaceHandle, + ) -> Result { + Err(Error::NotAvailable("Cache API primitives")) + } + + fn replace_get_stale_while_revalidate_ns( + &mut self, + memory: &mut wiggle::GuestMemory<'_>, + cache_handle: types::CacheReplaceHandle, + ) -> Result { + Err(Error::NotAvailable("Cache API primitives")) + } + + fn replace_get_state( + &mut self, + memory: &mut wiggle::GuestMemory<'_>, + cache_handle: types::CacheReplaceHandle, + ) -> Result { + Err(Error::NotAvailable("Cache API primitives")) + } + + fn replace_get_user_metadata( + &mut self, + memory: &mut wiggle::GuestMemory<'_>, + cache_handle: types::CacheReplaceHandle, + out_ptr: wiggle::GuestPtr, + out_len: u32, + nwritten_out: wiggle::GuestPtr, + ) -> Result<(), Error> { + Err(Error::NotAvailable("Cache API primitives")) + } + + fn replace_insert( + &mut self, + memory: &mut wiggle::GuestMemory<'_>, + cache_handle: types::CacheReplaceHandle, + options_mask: types::CacheWriteOptionsMask, + abi_options: wiggle::GuestPtr, + ) -> Result { + Err(Error::NotAvailable("Cache API primitives")) + } + fn transaction_lookup<'a>( &mut self, memory: &mut wiggle::GuestMemory<'_>, diff --git a/lib/wit/deps/fastly/compute.wit b/lib/wit/deps/fastly/compute.wit index 8d904b17..1006d781 100644 --- a/lib/wit/deps/fastly/compute.wit +++ b/lib/wit/deps/fastly/compute.wit @@ -970,9 +970,13 @@ interface cache { type handle = u32; /// Handle that can be used to check whether or not a cache lookup is waiting on another client. type busy-handle = u32; + /// Handle for an in-progress Replace operation + type replace-handle = u32; + type object-length = u64; type duration-ns = u64; type cache-hit-count = u64; + type cache-replace-strategy = u32; flags lookup-options-mask { request-headers, @@ -986,6 +990,19 @@ interface cache { request-headers: request-handle, } + flags replace-options-mask { + request-headers, + replace-strategy, + } + + record replace-options { + /** + * A full request handle, but used only for its headers + */ + request-headers: request-handle, + replace-strategy: cache-replace-strategy, + } + flags write-options-mask { reserved, request-headers, @@ -1005,7 +1022,7 @@ interface cache { /// - `transaction-insert-and-stream-back` /// - `transaction-update` /// - /// Some options are only allowed for certain of these hostcalls; see `cache-write-options-mask`. + /// Some options are only allowed for certain of these hostcalls; see `write-options-mask`. record write-options { /// this is a required field; there's no flag for it max-age-ns: duration-ns, @@ -1068,6 +1085,81 @@ interface cache { options: write-options, ) -> result; + /// The entrypoint to the replace API. + /// + /// This operation always participates in request collapsing and may return stale objects. + replace: func( + key: string, + options-mask: replace-options-mask, + options: replace-options, + ) -> result; + + /// Replace an object in the cache with the given metadata + /// + /// The returned handle is to a streaming body that is used for writing the object into + /// the cache. + replace-insert: func( + handle: replace-handle, + options-mask: write-options-mask, + options: write-options, + ) -> result; + + /// Gets the age of the existing object during replace, returning the + /// `$none` error if there was no object. + replace-get-age-ns: func( + handle: replace-handle, + ) -> result; + + /// Gets a range of the existing object body, returning the `$none` error if there + /// was no existing object. + /// + /// The returned `body_handle` must be closed before calling this function + /// again on the same `cache_replace_handle`. + replace-get-body: func( + handle: replace-handle, + options-mask: get-body-options-mask, + options: get-body-options, + ) -> result; + + /// Gets the number of cache hits for the existing object during replace, + /// returning the `$none` error if there was no object. + replace-get-hits: func( + handle: replace-handle, + ) -> result; + + /// Gets the content length of the existing object during replace, + /// returning the `$none` error if there was no object, or no content + /// length was provided. + replace-get-length: func( + handle: replace-handle, + ) -> result; + + /// Gets the configured max age of the existing object during replace, + /// returning the `$none` error if there was no object. + replace-get-max-age-ns: func( + handle: replace-handle, + ) -> result; + + /// Gets the configured stale-while-revalidate period of the existing + /// object during replace, returning the `$none` error if there was no + /// object. + replace-get-stale-while-revalidate-ns: func( + handle: replace-handle, + ) -> result; + + /// Gets the lookup state of the existing object during replace, returning + /// the `$none` error if there was no object. + replace-get-state: func( + handle: replace-handle, + ) -> result; + + /// Gets the user metadata of the existing object during replace, returning + /// the `$none` error if there was no object. + replace-get-user-metadata: func( + handle: replace-handle, + max-len: u64, + ) -> result>, error>; + /// The entrypoint to the request-collapsing cache transaction API. /// /// This operation always participates in request collapsing and may return stale objects. To bypass