pub struct TaggedCache<C: CacheStore> { /* private fields */ }Expand description
Cache with tag-based invalidation support.
The tag -> member-key index (and its reverse, key -> tags) is persisted in
the backing CacheStore itself under reserved keys — it is NOT kept in
a local, per-process map. That means the index is visible to every
instance sharing the same backing store (e.g. every app process pointed at
the same Redis), so a key tagged by one instance can be looked up and
invalidated by another.
§Atomicity caveat (concurrent race)
Index updates go through CacheStore::set_add / CacheStore::set_remove /
CacheStore::set_members. Only backends that override these with a
native set type — RedisCache does, via SADD/SREM/SMEMBERS — update
the index atomically (see CacheStore::supports_atomic_sets). Other
backends (e.g. InMemoryCache, MemcachedCache) fall back to the
trait’s default, non-atomic read-modify-write, so concurrent
set_with_tags/invalidate_tag calls against the SAME tag from
different instances can race and lose an update. For distributed
deployments, wrap a RedisCache (or another backend that overrides the
set primitives) if you need that guarantee. Self::new logs a warning
once, at construction time, when the backing store doesn’t support
atomic sets.
§Partial-failure caveat (sequential, non-transactional updates)
Separately from the concurrent-race caveat above: Self::set_with_tags,
Self::delete, and Self::invalidate_tags each issue several
independent set_add/set_remove/delete calls in sequence. If an
early call in one of these sequences succeeds and a later one fails, the
tag index (and its reverse index) can end up partially updated —
inconsistent with the cached value, or inconsistent with itself (e.g. a
key’s reverse-index tag set can end up not matching the forward tag ->
keys sets it’s actually a member of). There is currently no automatic
rollback or reconciliation for this case: a failed call may need to be
retried or the affected tag(s)/key(s) reconciled manually. A best-effort
warning is logged when this happens (see the calls guarded by
warn_on_partial_failure in the implementation) so operators at least
get a signal, but the index itself is not repaired automatically.
§Reserved key namespace
The tag index’s bookkeeping keys (tag_set_key/key_tags_set_key/
TAG_INDEX_KEY) all begin with the reserved prefix
"__armature_" and live in the SAME keyspace as caller-supplied keys —
both go through the same backing CacheStore, with only key_prefix
(from CacheConfig) applied identically to both. A caller-supplied key
that happens to start with "__armature_" would therefore collide with
this reserved bookkeeping keyspace (e.g. writing to
__armature_tag__:users would clobber the “users” tag’s member set).
This prefix is forbidden for caller-supplied keys: Self::set_with_tags,
Self::get, and Self::delete all reject a key starting with
"__armature_" with a CacheError::Config, rather than silently
allowing the collision.
Implementations§
Source§impl<C: CacheStore> TaggedCache<C>
impl<C: CacheStore> TaggedCache<C>
Sourcepub fn new(cache: Arc<C>) -> Self
pub fn new(cache: Arc<C>) -> Self
Create new tagged cache
Checks CacheStore::supports_atomic_sets on cache and logs a
warning once, here at construction time, when the backing store does
NOT support atomic sets — see the struct-level “Atomicity caveat”
docs for what that means for concurrent tag-index updates.
§Examples
use armature_cache::*;
let cache = RedisCache::new(config).await?;
let tagged = TaggedCache::new(Arc::new(cache));Set a value with tags
A repeated call for the same key REPLACES its tag membership with
tags (it does not union with whatever tags the key carried before).
§TTL and the tag index
ttl applies to the value, and is mirrored onto the key’s reverse
index (key -> tags) so that bookkeeping disappears along with the
value instead of outliving it forever.
The forward index (tag -> keys) cannot carry the same TTL: one tag set
holds many keys with independent lifetimes, so expiring the set would
drop the memberships of keys that are still alive. An expired key
therefore remains a member of its tags until it is noticed, and
Self::get_keys_by_tag reconciles that on read — it filters out
members whose value is gone and prunes them from the tag set, so tag
sets do not grow monotonically and stale keys are never returned.
§Examples
tagged.set_with_tags(
"user:123",
user_json,
&["users", "active-users"],
Some(Duration::from_secs(3600)),
).await?;Sourcepub async fn delete(&self, key: &str) -> CacheResult<()>
pub async fn delete(&self, key: &str) -> CacheResult<()>
Delete a specific key
Sourcepub async fn invalidate_tag(&self, tag: &str) -> CacheResult<()>
pub async fn invalidate_tag(&self, tag: &str) -> CacheResult<()>
Sourcepub async fn get_keys_by_tag(&self, tag: &str) -> CacheResult<Vec<String>>
pub async fn get_keys_by_tag(&self, tag: &str) -> CacheResult<Vec<String>>
Get all keys currently tagged with tag.
Members whose value is no longer present — most commonly because the
value’s TTL elapsed — are reconciled away rather than returned: the
forward index (tag -> keys) cannot carry the value’s TTL (one tag set
spans many keys with independent lifetimes), so an expired key would
otherwise stay a member of every one of its tags forever, growing the
tag sets monotonically and handing callers keys that no longer exist.
Reconciliation is best-effort: if pruning the stale members fails, the live keys are still returned and the prune is retried on the next read.
Get all tags for a specific key
Get all registered tags (tags currently carrying at least one member).