Skip to main content

Cache

Trait Cache 

Source
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 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§

Source

fn name(&self) -> &str

A label for diagnostics — "memory", "redis", "memcached".

Source

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.

Source

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.

Source

fn forget<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>>

Remove a key. true if it was there.

Source

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.

Source

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.

Source

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.

DriverHow
memoryinside the same mutex as the map
RedisSET key value NX PX ttl
Memcachedthe add command, which is atomic by definition
DynamoDBPutItem with a condition on the key not existing
Source

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§

Source

fn is_shared(&self) -> bool

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.

Source

fn has<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>>

Whether a key is present and unexpired.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§