1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
//! The Compute Simple Cache API.
//!
//! This is a non-durable key-value API backed by the same cache platform as the [Core Cache
//! API][core].
//!
//! ## Cache scope and purging
//!
//! Cache entries are scoped to Fastly [points of presence
//! (POPs)](https://developer.fastly.com/learning/concepts/pop/): the value set for a key in one POP
//! will not be visible in any other POP.
//!
//! Purging is also scoped to a POP by default, but can be configured to purge globally with
//! Fastly's [purging feature](https://developer.fastly.com/learning/concepts/purging/).
//!
//! ## Interoperability
//!
//! The Simple Cache API is implemented in terms of the [Core Cache API][core]. Items inserted with
//! the Core Cache API can be read by the Simple Cache API, and vice versa. However, some metadata
//! and advanced features like revalidation may be not be available via the Simple Cache API.
use fastly_shared::FastlyStatus;
use sha2::{Digest, Sha256};
use crate::http::purge::purge_surrogate_key;
use crate::Body;
pub use super::core::CacheKey;
use super::core::{self, Transaction};
use std::fmt::Write as _;
use std::time::Duration;
/// Errors arising from cache operations.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CacheError {
/// Operation failed due to a limit.
#[error("Simple Cache operation failed due to a limit")]
LimitExceeded,
/// An underlying Core Cache API operation found an invalid state.
///
/// This should not arise during use of this API. If encountered, please report it as a bug.
#[error("invalid Simple Cache operation; please report this as a bug")]
InvalidOperation,
/// Cache operation is not supported.
#[error("unsupported Simple Cache operation")]
Unsupported,
/// An IO error occurred during an operation.
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
/// An error occurred when purging a value.
#[error("purging error: {0}")]
Purge(#[source] crate::Error),
/// An error occurred while running the closure argument of [`get_or_set()`].
///
/// This uses [`anyhow::Error`] to provide maximum flexibility in how the closure reports errors.
#[error("get_or_set closure error: {0}")]
GetOrSet(#[source] anyhow::Error),
/// An unknown error occurred.
#[error("unknown Simple Cache operation error; please report this as a bug: {0:?}")]
Other(FastlyStatus),
}
impl From<core::CacheError> for CacheError {
fn from(value: core::CacheError) -> Self {
match value {
core::CacheError::LimitExceeded => Self::LimitExceeded,
core::CacheError::InvalidOperation => Self::InvalidOperation,
core::CacheError::Unsupported => Self::Unsupported,
core::CacheError::Other(st) => Self::Other(st),
}
}
}
/// Get the entry associated with the given cache key, if it exists.
///
/// ```no_run
/// # use fastly::cache::simple::*;
/// if let Some(value) = get("my_key").unwrap() {
/// let cached_string = value.into_string();
/// println!("the cached string was: {cached_string}");
/// }
/// ```
///
#[doc = include_str!("../../docs/snippets/key-argument.md")]
pub fn get(key: impl Into<CacheKey>) -> Result<Option<Body>, CacheError> {
let Some(found) = core::lookup(key.into()).execute()? else {
return Ok(None);
};
Ok(Some(found.to_stream()?))
}
/// Get the entry associated with the given cache key if it exists, or insert and return the
/// specified entry.
///
/// If the value is costly to compute, consider using [`get_or_set_with()`] instead to avoid
/// computation in the case where the value is already present.
///
/// ```no_run
/// # use fastly::cache::simple::*;
/// # use std::time::Duration;
/// let value = get_or_set("my_key", "hello!", Duration::from_secs(60)).unwrap();
/// let cached_string = value.into_string();
/// println!("the cached string was: {cached_string}");
/// ```
///
#[doc = include_str!("../../docs/snippets/key-body-argument.md")]
pub fn get_or_set(
key: impl Into<CacheKey>,
value: impl Into<Body>,
ttl: Duration,
) -> Result<Body, CacheError> {
get_or_set_with(key, || {
Ok(CacheEntry {
value: value.into(),
ttl,
})
})
.map(|opt| opt.expect("provided closure is infallible"))
}
/// The return type of the closure provided to [`get_or_set_with()`].
#[derive(Debug)]
pub struct CacheEntry {
/// The value to cache.
///
#[doc = include_str!("../../docs/snippets/body-argument.md")]
pub value: Body,
/// The time-to-live for the cache entry.
pub ttl: Duration,
}
/// Get the entry associated with the given cache key if it exists, or insert and return an entry
/// specified by running the given closure.
///
/// The closure is only run when no value is present for the key, and no other client is in the
/// process of setting it. It takes no arguments, and returns either `Ok` with a [`CacheEntry`]
/// describing the entry to set, or `Err` with an [`anyhow::Error`]. The error is not interpreted by
/// the API, and is solely provided as a user convenience. You can return an error for any reason,
/// and no value will be cached.
///
#[doc = include_str!("../../docs/snippets/key-argument.md")]
///
/// ## Example successful insertion
///
/// ```no_run
/// # use fastly::cache::simple::*;
/// # use std::time::Duration;
/// let value = get_or_set_with("my_key", || {
/// Ok(CacheEntry {
/// value: "hello!".into(),
/// ttl: Duration::from_secs(60),
/// })
/// })
/// .unwrap()
/// .expect("closure always returns `Ok`, so we have a value");
/// let cached_string = value.into_string();
/// println!("the cached string was: {cached_string}");
/// ```
///
/// ## Example unsuccessful insertion
///
/// ```no_run
/// # use fastly::cache::simple::*;
/// let mut tried_to_set = false;
/// let result = get_or_set_with("my_key", || {
/// tried_to_set = true;
/// anyhow::bail!("I changed my mind!")
/// });
/// if tried_to_set {
/// // if our closure was run, we can observe its error in the result
/// assert!(matches!(result, Err(CacheError::GetOrSet(_))));
/// }
/// ```
pub fn get_or_set_with<F>(
key: impl Into<CacheKey>,
make_entry: F,
) -> Result<Option<Body>, CacheError>
where
F: FnOnce() -> Result<CacheEntry, anyhow::Error>,
{
let key = key.into();
let lookup_tx = Transaction::lookup(key.clone()).execute()?;
if !lookup_tx.must_insert_or_update() {
if let Some(found) = lookup_tx.found() {
// the value is already present, so just return it
return Ok(Some(found.to_stream()?));
} else {
// we're not in the insert-or-update case, but there's no found?
return Err(CacheError::InvalidOperation);
}
}
// run the user-provided closure to produce the entry, tagging it as a user error if something
// goes wrong
let CacheEntry { value, ttl } = make_entry().map_err(CacheError::GetOrSet)?;
// perform a standard insert-and-read-back
let (mut insert_body, found) = lookup_tx
.insert(ttl)
.surrogate_keys([
surrogate_key_for_cache_key(&key, PurgeScope::Pop).as_str(),
surrogate_key_for_cache_key(&key, PurgeScope::Global).as_str(),
])
.execute_and_stream_back()?;
insert_body.append(value.into());
insert_body.finish()?;
Ok(Some(found.to_stream()?))
}
/// Insert an entry at the given cache key with the given time-to-live.
///
#[doc = include_str!("../../docs/snippets/key-body-argument.md")]
// TODO ACF 2023-06-27: expose this once the invalidation issue is resolved
#[allow(unused)]
fn set(key: impl Into<CacheKey>, value: impl Into<Body>, ttl: Duration) -> Result<(), CacheError> {
let key = key.into();
let mut insert_body = core::insert(key.clone(), ttl)
.surrogate_keys([
surrogate_key_for_cache_key(&key, PurgeScope::Pop).as_str(),
surrogate_key_for_cache_key(&key, PurgeScope::Global).as_str(),
])
.execute()?;
insert_body.append(value.into());
Ok(insert_body.finish()?)
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
enum PurgeScope {
#[default]
Pop,
Global,
}
/// Options for [`purge_with_opts()`].
#[derive(Copy, Clone, Debug, Default)]
pub struct PurgeOptions {
scope: PurgeScope,
}
impl PurgeOptions {
/// Purge the key from the current POP (default behavior).
///
/// This is the default option used by [`purge()`], and allows a higher throughput of purging
/// than purging globally.
pub fn pop_scope() -> Self {
Self {
scope: PurgeScope::Pop,
}
}
/// Purge the key globally.
///
#[doc = include_str!("../../docs/snippets/global-purge.md")]
pub fn global_scope() -> Self {
Self {
scope: PurgeScope::Global,
}
}
}
/// Purge the entry associated with the given cache key.
///
/// To configure the behavior of the purge, such as to purge globally rather than within the POP,
/// use [`purge_with_opts()`].
///
/// ## Note
///
/// Purged values may persist in cache for a short time (~150ms or less) after this function
/// returns.
///
#[doc = include_str!("../../docs/snippets/key-argument.md")]
pub fn purge(key: impl Into<CacheKey>) -> Result<(), CacheError> {
purge_surrogate_key(&surrogate_key_for_cache_key(
&key.into(),
PurgeOptions::default().scope,
))
.map_err(CacheError::Purge)
}
/// Purge the entry associated with the given cache key.
///
/// The [`PurgeOptions`] argument determines the scope of the purge operation.
///
/// ## Note
///
/// Purged values may persist in cache for a short time (~150ms or less) after this function
/// returns.
///
#[doc = include_str!("../../docs/snippets/key-argument.md")]
///
/// ## Example POP-scoped purge
///
/// ```no_run
/// # use fastly::cache::simple::*;
/// purge_with_opts("my_key", PurgeOptions::pop_scope()).unwrap();
/// ```
///
/// Note that this is the default behavior, and is therefore equivalent to:
///
/// ```no_run
/// # use fastly::cache::simple::*;
/// purge("my_key").unwrap();
/// ```
///
/// ## Example global-scoped purge
///
/// ```no_run
/// # use fastly::cache::simple::*;
/// purge_with_opts("my_key", PurgeOptions::global_scope()).unwrap();
/// ```
///
#[doc = include_str!("../../docs/snippets/global-purge.md")]
pub fn purge_with_opts(key: impl Into<CacheKey>, opts: PurgeOptions) -> Result<(), CacheError> {
purge_surrogate_key(&surrogate_key_for_cache_key(&key.into(), opts.scope))
.map_err(CacheError::Purge)
}
/// Create surrogate keys for the given cache key that is compatible with uses of the Simple Cache
/// API.
///
/// Each cache entry for the Simple Cache API is configured with surrogate keys from this function
/// in order to properly scope purge operations for POP-local and global purges. This function is
/// provided as a convenience for implementors wishing to add such a surrogate key manually via the
/// [Core Cache API][core] for interoperability with [`delete()`].
fn surrogate_key_for_cache_key(key: &CacheKey, scope: PurgeScope) -> String {
let mut sha = Sha256::new();
sha.update(key);
if let PurgeScope::Pop = scope {
// if FASTLY_POP is empty or unavailable for some reason, this will amount to a global purge
// for now which is the safer choice
if let Some(pop) = std::env::var_os("FASTLY_POP") {
sha.update(pop.to_string_lossy().as_bytes());
}
}
let mut sk_str = String::new();
for b in sha.finalize() {
write!(&mut sk_str, "{b:02X}").expect("writing to a String is infallible");
}
sk_str
}