pub trait Cache:
Send
+ Sync
+ 'static {
// Required methods
fn name(&self) -> &str;
fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Vec<u8>>>>;
fn put<'a>(
&'a self,
key: &'a str,
value: &'a [u8],
ttl: Option<Duration>,
) -> BoxFuture<'a, Result<()>>;
fn forget<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>>;
fn flush(&self) -> BoxFuture<'_, Result<()>>;
fn increment<'a>(
&'a self,
key: &'a str,
by: i64,
) -> BoxFuture<'a, Result<i64>>;
fn add<'a>(
&'a self,
key: &'a str,
value: &'a [u8],
ttl: Option<Duration>,
) -> BoxFuture<'a, Result<bool>>;
fn forget_if<'a>(
&'a self,
key: &'a str,
expected: &'a [u8],
) -> BoxFuture<'a, Result<bool>>;
// Provided methods
fn is_shared(&self) -> bool { ... }
fn supports_atomic_add(&self) -> bool { ... }
fn has<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>> { ... }
}Expand description
A key/value store with expiry.
Bytes rather than strings, because the interesting things to cache — a serialised session, a rendered page, a protobuf — are not all text, and forcing them through UTF-8 would mean base64 on top of an already-encoded value.
Every method is fallible and a miss is not a failure: get returns
Ok(None) for an absent key and Err only when the cache itself could not
be reached. Conflating the two is how a cache outage becomes an outage.
Required Methods§
Sourcefn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Vec<u8>>>>
fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Vec<u8>>>>
The value at key, or None if it is absent or expired.
Sourcefn put<'a>(
&'a self,
key: &'a str,
value: &'a [u8],
ttl: Option<Duration>,
) -> BoxFuture<'a, Result<()>>
fn put<'a>( &'a self, key: &'a str, value: &'a [u8], ttl: Option<Duration>, ) -> BoxFuture<'a, Result<()>>
Store value, expiring after ttl. None means no expiry.
Sourcefn forget<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>>
fn forget<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>>
Remove a key. true if it was there.
Sourcefn flush(&self) -> BoxFuture<'_, Result<()>>
fn flush(&self) -> BoxFuture<'_, Result<()>>
Remove everything.
On a shared server this removes everything in the database, not just
this application’s keys — which is why prefix
exists and why this is not something to call on a schedule.
Sourcefn increment<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>>
fn increment<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>>
Add by to a counter, creating it at zero first. Returns the new value.
Atomic where the driver can be — which is the point of having it on the port rather than as a get-then-put in application code, because that version loses increments under concurrency.
Sourcefn add<'a>(
&'a self,
key: &'a str,
value: &'a [u8],
ttl: Option<Duration>,
) -> BoxFuture<'a, Result<bool>>
fn add<'a>( &'a self, key: &'a str, value: &'a [u8], ttl: Option<Duration>, ) -> BoxFuture<'a, Result<bool>>
Store value only if key is absent. true if it was stored.
§This must be atomic
Not “atomic where convenient” — atomic. It is the primitive
Lock is built from, and therefore the primitive behind
without_overlapping and on_one_server. A has followed by a put
satisfies the signature and satisfies no caller: two processes both see
the key absent, both write, both believe they hold the lock, and the
scheduled task runs twice.
It is a required method with no default for exactly that reason. A default would be inherited silently by the next driver somebody adds, and the failure it causes is rare, non-local and expensive.
| Driver | How |
|---|---|
| memory | inside the same mutex as the map |
| Redis | SET key value NX PX ttl |
| Memcached | the add command, which is atomic by definition |
| DynamoDB | PutItem with a condition on the key not existing |
Sourcefn forget_if<'a>(
&'a self,
key: &'a str,
expected: &'a [u8],
) -> BoxFuture<'a, Result<bool>>
fn forget_if<'a>( &'a self, key: &'a str, expected: &'a [u8], ) -> BoxFuture<'a, Result<bool>>
Remove key only if it currently holds expected. true if it was
removed.
The other half of a lock, and the half people leave out.
Releasing with a plain forget is wrong in a way that
only shows up under load. A holder that stalls past its TTL — a long GC
pause, a slow query, a suspended VM — has already lost the lock; another
process has taken it. When the first one finally finishes and deletes
the key, it deletes someone else’s lock, and now a third process can
take it while the second still thinks it holds it.
Comparing the value first closes that. The value is the holder’s random token, so “is this still mine” has an answer.
Also required, and for the same reason as add.
Provided Methods§
Whether this store is visible to other processes.
The question anything cached for correctness has to ask: a lock, a rate limit, a “has this webhook been handled” marker. Over a per-process store each instance answers from its own copy, which for a lock means every instance holds it at once.
Defaults to true, because almost every implementation is a server
somewhere. An in-process one must override it — silence here is what
makes the failure quiet.
Sourcefn supports_atomic_add(&self) -> bool
fn supports_atomic_add(&self) -> bool
Whether add is genuinely atomic here.
true for everything that can compare-and-set — every driver in this
crate except Cloudflare Workers KV, which has no such operation at all.
It matters because add is the primitive a lock is built from. A
store where two callers can both “win” an add cannot hold a lock, and
the failure is silent: both callers proceed, both believe they are the
only one, and the report goes out twice. So
LockManager::is_shared requires this
as well as is_shared, which is what makes the
scheduler refuse rather than trusting an operator to have read the
driver’s documentation.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".