use crate::{
handle::{BodyHandle, RequestHandle, ResponseHandle, StreamingBodyHandle},
Response,
};
use bytes::Bytes;
use fastly_shared::FastlyStatus;
use fastly_sys::{
fastly_cache::{CacheDurationNs, CacheHitCount, CacheLookupState, CacheObjectLength},
fastly_http_cache as sys,
};
use std::{ptr, time::Duration};
const INITIAL_BUF_SIZE: usize = 4096;
pub type CacheKey = Bytes;
pub use sys::HttpStorageAction;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum HttpCacheError {
#[error("cache operation failed due to a limit")]
LimitExceeded,
#[error("invalid cache operation")]
InvalidOperation,
#[error("unsupported cache operation")]
Unsupported,
#[error("invalid backend")]
InvalidBackend,
#[error("unknown cache operation error; please report this as a bug: {0:?}")]
Other(FastlyStatus),
}
impl From<FastlyStatus> for HttpCacheError {
fn from(status: FastlyStatus) -> Self {
match status {
FastlyStatus::UNSUPPORTED => HttpCacheError::Unsupported,
FastlyStatus::LIMITEXCEEDED => HttpCacheError::LimitExceeded,
FastlyStatus::INVAL => HttpCacheError::InvalidBackend,
FastlyStatus::BADF => HttpCacheError::InvalidOperation,
other => HttpCacheError::Other(other),
}
}
}
impl HttpCacheError {
pub fn is_unsupported(&self) -> bool {
matches!(self, HttpCacheError::Unsupported)
}
}
#[derive(Debug, Default)]
pub struct LookupOptions {
pub override_key: Option<CacheKey>,
pub backend_name: String,
}
impl LookupOptions {
fn as_abi(&self) -> (sys::HttpCacheLookupOptionsMask, sys::HttpCacheLookupOptions) {
use sys::HttpCacheLookupOptionsMask as Mask;
let mut mask = Mask::empty();
let (override_key_ptr, override_key_len) = if let Some(v) = &self.override_key {
mask.insert(Mask::OVERRIDE_KEY);
(v.as_ptr(), v.len())
} else {
(ptr::null(), 0)
};
let (backend_name_ptr, backend_name_len) =
(self.backend_name.as_ptr(), self.backend_name.len());
mask.insert(Mask::BACKEND_NAME);
let options = sys::HttpCacheLookupOptions {
override_key_ptr,
override_key_len,
backend_name_ptr,
backend_name_len,
};
(mask, options)
}
}
#[derive(Debug, Default, Clone)]
pub(crate) struct WriteOptions {
pub max_age: Duration,
pub vary_rule_abi: String,
pub initial_age: Duration,
pub stale_while_revalidate: Duration,
pub surrogate_keys_abi: String,
pub length: Option<u64>,
pub sensitive_data: bool,
}
fn duration_to_u64_ns(dur: Duration) -> u64 {
u64::try_from(dur.as_nanos()).unwrap_or(u64::MAX)
}
impl WriteOptions {
fn as_abi(&self) -> (sys::HttpCacheWriteOptionsMask, sys::HttpCacheWriteOptions) {
use sys::HttpCacheWriteOptionsMask as Mask;
let mut mask = Mask::VARY_RULE
| Mask::INITIAL_AGE_NS
| Mask::STALE_WHILE_REVALIDATE_NS
| Mask::SURROGATE_KEYS;
if self.sensitive_data {
mask.insert(Mask::SENSITIVE_DATA);
}
let length = if let Some(v) = self.length {
mask.insert(Mask::LENGTH);
v
} else {
0
};
let options = sys::HttpCacheWriteOptions {
max_age_ns: duration_to_u64_ns(self.max_age),
vary_rule_ptr: self.vary_rule_abi.as_ptr(),
vary_rule_len: self.vary_rule_abi.len(),
initial_age_ns: duration_to_u64_ns(self.initial_age),
stale_while_revalidate_ns: duration_to_u64_ns(self.stale_while_revalidate),
surrogate_keys_ptr: self.surrogate_keys_abi.as_ptr(),
surrogate_keys_len: self.surrogate_keys_abi.len(),
length,
};
(mask, options)
}
}
pub fn is_request_cacheable(req_handle: &RequestHandle) -> Result<bool, HttpCacheError> {
let mut is_cacheable_out = 0;
unsafe { sys::is_request_cacheable(req_handle.as_u32(), &mut is_cacheable_out) }.result()?;
Ok(is_cacheable_out == 1)
}
pub fn transaction_lookup(
req_handle: &RequestHandle,
options: &LookupOptions,
) -> Result<HttpCacheHandle, HttpCacheError> {
let mut cache_handle_out = HttpCacheHandle::INVALID;
let (options_mask, options) = options.as_abi();
unsafe {
sys::transaction_lookup(
req_handle.as_u32(),
options_mask,
&options,
cache_handle_out.as_abi_mut(),
)
}
.result()?;
Ok(cache_handle_out)
}
#[derive(Debug)]
pub struct HttpCacheHandle {
cache_handle: sys::HttpCacheHandle,
}
impl Drop for HttpCacheHandle {
fn drop(&mut self) {
if !self.is_invalid() {
unsafe {
let _ = sys::close(self.as_abi());
}
}
}
}
impl HttpCacheHandle {
#[cfg_attr(
not(target_env = "p1"),
deprecated(
since = "0.11.6",
note = "This code will need to be updated for wasip2."
)
)]
const INVALID: Self = HttpCacheHandle {
cache_handle: sys::INVALID_HTTP_CACHE_HANDLE,
};
fn is_invalid(&self) -> bool {
self.cache_handle == sys::INVALID_HTTP_CACHE_HANDLE
}
fn as_abi(&self) -> sys::HttpCacheHandle {
self.cache_handle
}
fn as_abi_mut(&mut self) -> &mut sys::HttpCacheHandle {
&mut self.cache_handle
}
pub fn transaction_insert(
&self,
resp_handle: ResponseHandle,
options: &WriteOptions,
) -> Result<StreamingBodyHandle, HttpCacheError> {
let mut body_handle_out = BodyHandle::INVALID;
let (options_mask, options) = options.as_abi();
unsafe {
sys::transaction_insert(
self.as_abi(),
resp_handle.into_u32(),
options_mask,
&options,
body_handle_out.as_u32_mut(),
)
}
.result()?;
Ok(StreamingBodyHandle::from_body_handle(body_handle_out))
}
pub fn transaction_insert_and_stream_back(
&self,
resp_handle: ResponseHandle,
options: &WriteOptions,
) -> Result<(StreamingBodyHandle, HttpCacheHandle), HttpCacheError> {
let mut body_handle_out = BodyHandle::INVALID;
let mut cache_handle_out = HttpCacheHandle::INVALID;
let (options_mask, options) = options.as_abi();
unsafe {
sys::transaction_insert_and_stream_back(
self.as_abi(),
resp_handle.into_u32(),
options_mask,
&options,
body_handle_out.as_u32_mut(),
cache_handle_out.as_abi_mut(),
)
}
.result()?;
let streaming_body_handle = StreamingBodyHandle::from_body_handle(body_handle_out);
Ok((streaming_body_handle, cache_handle_out))
}
pub fn transaction_update(
&self,
resp_handle: ResponseHandle,
options: &WriteOptions,
) -> Result<(), HttpCacheError> {
let (options_mask, options) = options.as_abi();
unsafe {
sys::transaction_update(
self.as_abi(),
resp_handle.into_u32(),
options_mask,
&options,
)
}
.result()
.map_err(Into::into)
}
pub fn transaction_update_and_return_fresh(
&self,
resp_handle: ResponseHandle,
options: &WriteOptions,
) -> Result<HttpCacheHandle, HttpCacheError> {
let mut cache_handle_out = HttpCacheHandle::INVALID;
let (options_mask, options) = options.as_abi();
unsafe {
sys::transaction_update_and_return_fresh(
self.as_abi(),
resp_handle.into_u32(),
options_mask,
&options,
cache_handle_out.as_abi_mut(),
)
}
.result()?;
Ok(cache_handle_out)
}
pub fn transaction_record_not_cacheable(
&self,
max_age: Duration,
vary_rule_abi: &str,
) -> Result<(), HttpCacheError> {
let options_mask = sys::HttpCacheWriteOptionsMask::VARY_RULE;
let options = sys::HttpCacheWriteOptions {
max_age_ns: duration_to_u64_ns(max_age),
vary_rule_ptr: vary_rule_abi.as_ptr(),
vary_rule_len: vary_rule_abi.len(),
initial_age_ns: 0,
stale_while_revalidate_ns: 0,
surrogate_keys_ptr: std::ptr::null(),
surrogate_keys_len: 0,
length: 0,
};
unsafe { sys::transaction_record_not_cacheable(self.as_abi(), options_mask, &options) }
.result()
.map_err(Into::into)
}
pub fn transaction_abandon(&self) -> Result<(), HttpCacheError> {
unsafe { sys::transaction_abandon(self.as_abi()) }
.result()
.map_err(Into::into)
}
pub(crate) fn wait(&self) -> Result<(), HttpCacheError> {
let mut cache_lookup_state_out = CacheLookupState::empty();
unsafe { sys::get_state(self.as_abi(), &mut cache_lookup_state_out) }
.result()
.map_err(Into::into)
}
pub fn get_suggested_backend_request(&self) -> Result<RequestHandle, HttpCacheError> {
let mut req_handle_out = RequestHandle::INVALID;
unsafe { sys::get_suggested_backend_request(self.as_abi(), req_handle_out.as_u32_mut()) }
.result()?;
Ok(req_handle_out)
}
pub fn get_suggested_cache_options(&self, resp_handle: &ResponseHandle) -> WriteOptions {
use sys::HttpCacheWriteOptionsMask as Mask;
let wanted_mask = Mask::all();
let mut vary_rule_buf = Vec::with_capacity(INITIAL_BUF_SIZE);
let mut surrogate_keys_buf = Vec::with_capacity(INITIAL_BUF_SIZE);
let mut guest_supplied_pointers = sys::HttpCacheWriteOptions {
max_age_ns: 0, vary_rule_ptr: vary_rule_buf.as_mut_ptr(),
vary_rule_len: vary_rule_buf.capacity(),
initial_age_ns: 0, stale_while_revalidate_ns: 0, surrogate_keys_ptr: surrogate_keys_buf.as_mut_ptr(),
surrogate_keys_len: surrogate_keys_buf.capacity(),
length: 0, };
let mut options_mask_out = Mask::empty();
let mut options_out = sys::HttpCacheWriteOptions {
max_age_ns: 0,
vary_rule_ptr: ptr::null_mut(),
vary_rule_len: 0,
initial_age_ns: 0,
stale_while_revalidate_ns: 0,
surrogate_keys_ptr: ptr::null_mut(),
surrogate_keys_len: 0,
length: 0,
};
let mut status = unsafe {
sys::get_suggested_cache_options(
self.as_abi(),
resp_handle.as_u32(),
wanted_mask,
&guest_supplied_pointers,
&mut options_mask_out,
&mut options_out,
)
};
if let FastlyStatus::BUFLEN = status {
if options_mask_out.contains(Mask::VARY_RULE)
&& options_out.vary_rule_len > vary_rule_buf.len()
{
vary_rule_buf.reserve_exact(options_out.vary_rule_len);
guest_supplied_pointers.vary_rule_ptr = vary_rule_buf.as_mut_ptr();
guest_supplied_pointers.vary_rule_len = options_out.vary_rule_len;
}
if options_mask_out.contains(Mask::SURROGATE_KEYS)
&& options_out.surrogate_keys_len > surrogate_keys_buf.len()
{
surrogate_keys_buf.reserve_exact(options_out.surrogate_keys_len);
guest_supplied_pointers.surrogate_keys_ptr = surrogate_keys_buf.as_mut_ptr();
guest_supplied_pointers.surrogate_keys_len = options_out.surrogate_keys_len;
}
options_mask_out = Mask::empty();
status = unsafe {
sys::get_suggested_cache_options(
self.as_abi(),
resp_handle.as_u32(),
wanted_mask,
&guest_supplied_pointers,
&mut options_mask_out,
&mut options_out,
)
};
}
if status.is_err() {
panic!(
"get_suggested_cache options unexpectedly failed: {:?}",
status
)
}
let vary_rule_abi = {
unsafe { vary_rule_buf.set_len(options_out.vary_rule_len) };
String::from_utf8(vary_rule_buf).expect("host should only return UTF-8")
};
let surrogate_keys_abi = {
unsafe { surrogate_keys_buf.set_len(options_out.surrogate_keys_len) };
String::from_utf8(surrogate_keys_buf).expect("host should only return UTF-8")
};
let length = if options_mask_out.contains(Mask::LENGTH) {
Some(options_out.length)
} else {
None
};
WriteOptions {
max_age: Duration::from_nanos(options_out.max_age_ns),
vary_rule_abi,
initial_age: Duration::from_nanos(options_out.initial_age_ns),
stale_while_revalidate: Duration::from_nanos(options_out.stale_while_revalidate_ns),
surrogate_keys_abi,
length,
sensitive_data: options_mask_out.contains(Mask::SENSITIVE_DATA),
}
}
pub fn prepare_response_for_storage(
&self,
resp_handle: &mut ResponseHandle,
) -> Result<HttpStorageAction, HttpCacheError> {
let mut resp_handle_out = ResponseHandle::INVALID;
let mut storage_action_out = sys::HttpStorageAction::DoNotStore;
unsafe {
sys::prepare_response_for_storage(
self.as_abi(),
resp_handle.as_u32(),
&mut storage_action_out,
resp_handle_out.as_u32_mut(),
)
}
.result()?;
*resp_handle = resp_handle_out;
Ok(storage_action_out)
}
pub fn get_found_response(&self, was_hit: bool) -> Result<Response, HttpCacheError> {
let mut resp_handle_out = ResponseHandle::INVALID;
let mut body_handle_out = BodyHandle::INVALID;
unsafe {
sys::get_found_response(
self.as_abi(),
1,
resp_handle_out.as_u32_mut(),
body_handle_out.as_u32_mut(),
)
}
.result()?;
let mut resp = Response::from_handles(resp_handle_out, body_handle_out);
resp.metadata.cache_options = Some(WriteOptions {
max_age: Duration::from_nanos(
self.get_max_age_ns().expect("cache options are present"),
),
vary_rule_abi: self.get_vary_rule_abi().expect("cache options are present"),
initial_age: Duration::from_nanos(
self.get_age_ns().expect("cache options are present"),
),
stale_while_revalidate: Duration::from_nanos(
self.get_stale_while_revalidate_ns()
.expect("cache options are present"),
),
surrogate_keys_abi: self
.get_surrogate_keys_abi()
.expect("cache options are present"),
length: self.get_length(),
sensitive_data: self
.get_sensitive_data()
.expect("cache options are present"),
});
resp.metadata.cache_storage_action = None;
resp.metadata.cache_hits = self.get_hits().filter(|_| was_hit);
Ok(resp)
}
pub fn get_state(&self) -> CacheLookupState {
let mut cache_lookup_state_out = CacheLookupState::empty();
unsafe { sys::get_state(self.as_abi(), &mut cache_lookup_state_out) }
.result()
.expect("sys::get_state failed");
cache_lookup_state_out
}
pub fn must_insert_or_update(&self) -> bool {
self.get_state()
.contains(CacheLookupState::MUST_INSERT_OR_UPDATE)
}
pub fn get_length(&self) -> Option<CacheObjectLength> {
let mut length_out = 0;
let status = unsafe { sys::get_length(self.as_abi(), &mut length_out) };
match status {
FastlyStatus::OK => Some(length_out),
FastlyStatus::NONE => None,
status => {
panic!("sys::get_length failed with {status:?}");
}
}
}
pub fn get_max_age_ns(&self) -> Option<CacheDurationNs> {
let mut duration_out = 0;
let status = unsafe { sys::get_max_age_ns(self.as_abi(), &mut duration_out) };
match status {
FastlyStatus::OK => Some(duration_out),
FastlyStatus::NONE => None,
status => {
panic!("sys::get_max_age_ns failed with {status:?}");
}
}
}
pub fn get_stale_while_revalidate_ns(&self) -> Option<CacheDurationNs> {
let mut duration_out = 0;
let status =
unsafe { sys::get_stale_while_revalidate_ns(self.as_abi(), &mut duration_out) };
match status {
FastlyStatus::OK => Some(duration_out),
FastlyStatus::NONE => None,
status => {
panic!("sys::get_stale_while_revalidate_ns failed with {status:?}");
}
}
}
pub fn get_age_ns(&self) -> Option<CacheDurationNs> {
let mut duration_out = 0;
let status = unsafe { sys::get_age_ns(self.as_abi(), &mut duration_out) };
match status {
FastlyStatus::OK => Some(duration_out),
FastlyStatus::NONE => None,
status => {
panic!("sys::get_age_ns failed with {status:?}");
}
}
}
pub fn get_hits(&self) -> Option<CacheHitCount> {
let mut hits_out = 0;
let status = unsafe { sys::get_hits(self.as_abi(), &mut hits_out) };
match status {
FastlyStatus::OK => Some(hits_out),
FastlyStatus::NONE => None,
status => {
panic!("sys::get_hits failed with {status:?}");
}
}
}
pub fn get_sensitive_data(&self) -> Option<bool> {
let mut is_sensitive_out = 0;
let status = unsafe { sys::get_sensitive_data(self.as_abi(), &mut is_sensitive_out) };
match status {
FastlyStatus::OK => Some(is_sensitive_out == 1),
FastlyStatus::NONE => None,
status => {
panic!("sys::get_sensitive_data failed with {status:?}");
}
}
}
pub fn get_surrogate_keys_abi(&self) -> Option<String> {
let mut surrogate_keys_buf = Vec::with_capacity(INITIAL_BUF_SIZE);
let mut nwritten_out = 0;
let mut status = unsafe {
sys::get_surrogate_keys(
self.as_abi(),
surrogate_keys_buf.as_mut_ptr(),
surrogate_keys_buf.capacity(),
&mut nwritten_out,
)
};
if status == FastlyStatus::BUFLEN {
surrogate_keys_buf.reserve_exact(nwritten_out);
status = unsafe {
sys::get_surrogate_keys(
self.as_abi(),
surrogate_keys_buf.as_mut_ptr(),
surrogate_keys_buf.capacity(),
&mut nwritten_out,
)
};
}
match status {
FastlyStatus::OK => {
unsafe { surrogate_keys_buf.set_len(nwritten_out) };
Some(String::from_utf8(surrogate_keys_buf).expect("host should only return UTF-8"))
}
FastlyStatus::NONE => None,
_ => panic!("sys::get_surrogate_keys_abi failed with {status:?}"),
}
}
pub fn get_vary_rule_abi(&self) -> Option<String> {
let mut vary_rule_buf = Vec::with_capacity(INITIAL_BUF_SIZE);
let mut nwritten_out = 0;
let mut status = unsafe {
sys::get_vary_rule(
self.as_abi(),
vary_rule_buf.as_mut_ptr(),
vary_rule_buf.capacity(),
&mut nwritten_out,
)
};
if status == FastlyStatus::BUFLEN {
vary_rule_buf.reserve_exact(nwritten_out);
status = unsafe {
sys::get_vary_rule(
self.as_abi(),
vary_rule_buf.as_mut_ptr(),
vary_rule_buf.capacity(),
&mut nwritten_out,
)
};
}
match status {
FastlyStatus::OK => {
unsafe { vary_rule_buf.set_len(nwritten_out) };
Some(String::from_utf8(vary_rule_buf).expect("host should only return UTF-8"))
}
FastlyStatus::NONE => None,
_ => panic!("sys::get_vary_rule_abi failed with {status:?}"),
}
}
}