This document describes Memento's internal architecture. It's intended for contributors and those who want to understand how the library works under the hood.
Memento has a layered architecture:
┌─────────────────────────────────────────┐
│ memento.core (API) │ ← User-facing functions
├─────────────────────────────────────────┤
│ memento.mount (MountPoint) │ ← Function ↔ Cache binding
├─────────────────────────────────────────┤
│ memento.caffeine (CaffeineCache) │ ← Cache implementation
├─────────────────────────────────────────┤
│ Java classes (performance-critical) │ ← Low-level operations
└─────────────────────────────────────────┘
Cache (ICache): Stores key-value pairs. One cache can serve multiple functions.
MountPoint (IMountPoint): Connects a function to a cache. Contains:
This separation enables:
A Segment contains metadata about a memoized function binding:
public class Segment {
public final IFn f; // Original function
public final IFn keyFn; // Key transformation function
public final Object id; // Identifier (typically var name)
public final Object conf; // Mount configuration
}
Cache entries are keyed by CacheKey, which combines the segment ID with transformed arguments:
public class CacheKey {
public final Object id; // Segment identifier
public final Object args; // Transformed function arguments
}
This allows multiple functions to share a cache while keeping their entries separate.
Performance-critical code is implemented in Java to:
myns$myfn.invoke
clojure.lang.AFn.applyToHelper
clojure.lang.AFn.applyTo
clojure.core$apply.invokeStatic
clojure.core$apply.invoke
memento.caffeine.CaffeineCache$fn__2536.invoke
memento.caffeine.CaffeineCache.cached
memento.mount.UntaggedMountPoint.cached
memento.mount$bind$fn__2432.doInvoke
clojure.lang.RestFn.applyTo
clojure.lang.AFunction$1.doInvoke
clojure.lang.RestFn.invoke
myns$myfn.invoke
clojure.lang.AFn.applyToHelper
memento.caffeine.CaffeineCache$fn__2052.invoke
memento.caffeine.CaffeineCache.cached
memento.mount.CachedFn.invoke
From 11 stack frames to 4.
memento.baseICache: Core cache interface with methods like cached, invalidate, addEntriesSegment: Function binding metadataCacheKey: Composite key (id + args)EntryMeta: Wrapper for cached values with metadata (tag IDs, no-cache flag)LockoutMap: Coordinates bulk invalidationsDurations: Time unit conversionsmemento.mountIMountPoint: Interface for mount pointsCached: Marker interface for memoized functionsCachedFn: IFn implementation that delegates to mount pointCachedMultiFn: MultiFn wrapper for memoized multimethodsmemento.caffeineCaffeineCache_: Core Caffeine operationsSecondaryIndex: Maps tag+ID pairs to cache keys for bulk invalidationExpiry: Interface for variable per-entry expirySpecialPromise: Promise that tracks invalidation state during loadsmemento.multiMultiCache: Base class for tiered cachesTieredCache: Both caches updated on missConsultingCache: Only local updated on missDaisyChainCache: Local never updatedCaffeine ensures only one load happens per key. If multiple threads request the same uncached key simultaneously:
CompletableFutureIf a key is invalidated while being loaded:
SpecialPromise is marked invalidTag invalidation is more complex because:
The LockoutMap coordinates bulk invalidations:
public class LockoutMap {
// Map of [tag, id] -> CountDownLatch
// When invalidation starts, entry is added
// Loads check this map and wait if their tag is being invalidated
// When invalidation completes, latch is counted down and entry removed
}
When a tag is invalidated while a load is in progress for an entry with that tag:
The SecondaryIndex maintains mappings from tag+ID pairs to cache keys:
Tag: :user
ID: 123 -> #{CacheKey[get-user, [123]], CacheKey[get-orders, [123]]}
ID: 456 -> #{CacheKey[get-user, [456]]}
Tag: :order
ID: 789 -> #{CacheKey[get-order, [789]], CacheKey[get-order-items, [789]]}
When memo-clear-tag! is called:
Cached values are wrapped in EntryMeta which tracks:
noCache flag from do-not-cache)public class EntryMeta {
public final Object v; // The cached value
public final boolean noCache; // If true, don't cache this
public final Set tagIdents; // Set of [tag, id] pairs
}
In development, namespaces are frequently reloaded. When a memoized function's var is redefined:
Reload guards use Java finalizers to clean up:
Disable for production: -Dmemento.reloadable=false
(m/create {mc/ttl [5 :m]})
memento.base/new-cache multimethod dispatches on mc/typeCaffeine instance with configurationCaffeineCache record implementing ICache(m/bind #'get-user {} my-cache)
Segment with function, key-fn, id, configCachedFn(get-user 123)
CachedFn.invoke called with argsIMountPoint.cachedCacheKey from segment ID + transformed argsret-fn if configuredEntryMetaret-fn, extracts tag IDsnoCache flag set, returns without cachingImplement memento.base/ICache:
(defrecord MyCache [...]
ICache
(conf [this] ...)
(cached [this segment args] ...)
(ifCached [this segment args] ...)
(invalidate [this segment] ...)
(invalidate [this segment args] ...)
(invalidateAll [this] ...)
(invalidateIds [this tag-ids] ...)
(addEntries [this segment args-to-vals] ...)
(asMap [this] ...)
(asMap [this segment] ...))
Register with multimethod:
(defmethod memento.base/new-cache :my-cache-type
[conf]
(->MyCache ...))
Use:
(m/memo my-fn {mc/type :my-cache-type ...})
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |