clojure.core.cache

repository·master·Indexed 19 days ago

https://github.com/clojure/core.cache

A Clojure library providing in-memory caching strategies including FIFO, LRU, LFU, TTL, and LIRS. It offers two primary APIs: clojure.core.cache for immutable caches and clojure.core.cache.wrapped for caches wrapped in atoms. The library includes the CacheProtocol for implementing custom cache types and the defcache macro for convenience.

Tokens
6.5K
Snippets
18
Records
25
Agent score
66%

What's inside clojure.core.cache

  1. When to use an LRU cache

    master

    An LRU (Least-Recently-Used) cache is best suited for data subject to locality concerns, where recently accessed items are likely to be accessed again soon.

    Advantages

    • Simple and easy-to-understand logic.
    • Reasonably fast and efficient.
    • Performs better as cache size increases.
    • Agile in adapting to changing access patterns.

    Disadvantages

    • Memory usage: Requires more historical data to track access order, making it more memory-intensive than simpler caches like FIFO.
    • Access patterns: Performs poorly when elements are accessed occasionally but consistently while other elements are accessed very frequently for a short burst and then never again.
    • Efficiency: Requires a larger cache size to achieve high efficiency.
  2. How the LU cache eviction policy works

    master

    The LU (Least Used) cache, also known as Least Frequently Used, evicts items that have been accessed the least frequently once the :threshold is exceeded.

    Unlike an LRU (Least Recently Used) cache which cares about when an item was last accessed, the LU cache cares about how many times an item has been accessed. Even if an item was accessed more recently than another, it may be evicted if its total access count is lower.

    To increase an item's importance in the cache, use the .hit method to manually record an access.

    ;; Example of LU eviction based on frequency rather than recency
    (def C (cache/lu-cache-factory {} :threshold 2))
    
    (-> C 
        (assoc :a 1) 
        (assoc :b 2)
        (.hit :b)   ;; :b has 1 hit
        (.hit :b)   ;; :b has 2 hits
        (.hit :a)   ;; :a has 1 hit (more recent than :b, but less frequent)
        (assoc :c 3)) 
    
    ;; Result: {:c 3, :b 2} 
    ;; :a was evicted because it was used less frequently than :b,
    ;; despite :a being the most recently 'touched' item.
  3. Understand the difference between clojure.core.cache and clojure.core.cache.wrapped

    master

    The library provides two distinct namespaces that should not be mixed:

    1. clojure.core.cache: Provides an API for immutable in-memory caches. You are responsible for managing the storage of these data structures, typically by wrapping them in a Clojure atom.

    2. clojure.core.cache.wrapped: Provides the same API but operates on caches that are already wrapped in an atom. This is generally the more intuitive approach for most use cases. It includes specialized functions like lookup-or-miss to handle common caching patterns safely and efficiently.

    Recommendation: Use clojure.core.cache.wrapped for straightforward application state management to avoid the complexity of manually managing immutable cache updates with swap!.

  4. When to use LIRS instead of LRU

    master

    LIRS (Low Inter-Reference Recency Set) is a cache eviction policy that differs from LRU. While LRU relies solely on how recently an item was accessed, LIRS uses the access recency of other cache elements relative to any other block to determine eviction. This makes it more robust in certain access patterns where simple recency might lead to frequent cache misses (e.g., scanning patterns).

    Refer to the LRU documentation to compare specific behaviors and the Basic Usage patterns to understand how to interact with the resulting immutable map.

  5. When to use a TTL cache

    master

    The TTL cache eviction policy is simple and effective for data subject to temporal concerns (data that becomes invalid after a certain amount of time).

    Advantages

    • Simplicity: The logic is easy to understand.
    • Performance: It is reasonably fast.
    • Temporal Suitability: Works well for data where age is the primary concern.

    Disadvantages

    • Memory Overhead: The implementation is moderately memory intensive because it must track "age" information for items.
    • Data Requirements: It requires more historical data to operate compared to other strategies.
    • Efficiency: Cache size does not generally improve the efficiency of a TTL cache.

    Always measure your system's specific characteristics to determine if TTL is the best eviction strategy for your use case.

  6. How FIFO cache eviction works

    master

    In a FIFOCache, elements are evicted based on their insertion order. When the number of elements exceeds the :threshold, the element that has been in the cache the longest (the one at the front of the queue) is removed.

    Example of eviction behavior:

    1. Initialization: Create a cache with a threshold of 3 and seed it with {:a 1, :b 2, :c 3}.
    2. Single Eviction: Adding one new element (:d 42) will cause the first element added (:a) to be evicted.
    3. Multiple Evictions: Adding multiple elements will evict multiple older values in the order they were added.
    ;; Initialize with threshold 3
    (def C (cache/fifo-cache-factory {:a 1, :b 2, :c 3} :threshold 3))
    
    ;; Adding :d 42 evicts :a
    (assoc C :d 42)
    ;; => {:c 3, :b 2, :d 42}
    
    ;; Adding :x 36 and :z 138 evicts :b and :c
    (assoc C :x 36 :z 138)
    ;; => {:z 138, :x 36, :b 2}
  7. Compose multiple cache policies using factories

    master

    Since all core.cache types implement the CacheProtocol, you can compose multiple cache behaviors by nesting cache factories. This allows you to layer different eviction policies, such as combining a size-based eviction policy (like FIFO) with a time-based policy (like TTL).

    To compose caches, pass the result of one cache factory as the seed data to another. The outer cache wraps the inner cache, applying its policy to the entries managed by the inner cache.

    (def C (-> {:a 1 :b 2} 
           (fifo-cache-factory :threshold 2) 
           (ttl-cache-factory :ttl 5000)))
    
    ;; Within the TTL window, the cache evicts elements using FIFO policy:
    (assoc C :c 42)
    ;;=> {:b 2, :c 42}
    
    ;; After the TTL window expires, expired elements are also evicted:
    (assoc C :d 138)
    ;;=> {:d 138}
  8. Understand the CacheProtocol

    master

    The CacheProtocol is the core interface for all cache implementations in core.cache. To create a custom cache, you must implement these methods to define how values are retrieved, tracked, and evicted.

    Key methods include:

    • lookup [cache e] or lookup [cache e not-found]: Retrieve a value for key e.
    • has? [cache e]: Check if the cache contains key e.
    • hit [cache e]: Called when a cache hit occurs. Use this to update hit statistics or return a new version of the cache.
    • miss [cache e ret]: Called when a cache miss occurs. This is typically where eviction logic and new value insertion happen.
    • evict [cache e]: Removes an entry from the cache.
    • seed [cache base]: Signals the cache to be initialized with a base structure (e.g., a map of existing values).
    (defprotocol CacheProtocol
      "This is the protocol describing the basic cache capability."
      (lookup [cache e]
              [cache e not-found]
       "Retrieve the value associated with `e` if it exists, else `nil` in
       the 2-arg case.  Retrieve the value associated with `e` if it exists,
       else `not-found` in the 3-arg case.")
      (has?    [cache e]
       "Checks if the cache contains a value associated with `e`")
      (hit     [cache e]
       "Is meant to be called if the cache is determined to contain a value
       associated with `e`")
      (miss    [cache e ret]
       "Is meant to be called if the cache is determined to **not** contain a
       value associated with `e`")
      (evict  [cache e]
       "Removes an entry from the cache")
      (seed    [cache base]
       "Is used to signal that the cache should be created with a seed.
       The contract is that said cache should return an instance of its
       own type.")
  9. Use the immutable clojure.core.cache API

    master

    When using the clojure.core.cache namespace, you work with immutable data structures. To maintain state across updates, you must wrap the cache in an atom and use swap! to apply transformations.

    Common operations include:

    • through-cache: A shorthand to check for a key and, if missing, compute and insert a value.
    • evict: Removes a specific key from the cache.
    • has?, hit, miss: Low-level primitives for checking existence and retrieving values.
    (require '[clojure.core.cache :as cache])
    
    ;; Create an immutable FIFO cache
    (def C1 (cache/fifo-cache-factory {:a 1, :b 2}))
    
    ;; Manual lookup/miss logic
    (def C1' (if (cache/has? C1 :c)
               (cache/hit C1 :c)
               (cache/miss C1 :c 42)))
    
    ;; Shorthand using through-cache
    (def C1'' (cache/through-cache C1 :c (constantly 42)))
    
    ;; Managing state with an atom
    (def C2 (atom (cache/fifo-cache-factory {:a 1, :b 2})))
    (swap! C2 cache/through-cache :d (constantly 13))
    (swap! C2 cache/evict :b)
  10. Include core.cache in Leiningen projects

    master

    To use core.cache in a Leiningen project, add the following dependency to the :dependencies vector in your project.clj file. Replace the version string with the specific version you require.

    [org.clojure/core.cache "1.0.217"]
  11. When to use an LU cache

    master

    The LU cache eviction policy is simple and effective in specific scenarios, but has trade-offs depending on your access patterns.

    Advantages

    • Performs well if access patterns remain stable over time.
    • Logic is easy to understand and reason about.
    • Reasonably fast performance.
    • Works well with data subject to temporal locality concerns.

    Disadvantages

    • Performs poorly if access patterns change frequently.
    • Requires historical access data to function effectively.
    • Generally requires a larger cache size to achieve high efficiency compared to other strategies.

    Recommendation: Always measure your system's specific access characteristics to determine if LU is the best eviction strategy for your use case.

  12. Include core.cache in Maven projects

    master

    For Maven-driven projects, add the following <dependency> block to the <dependencies> section of your pom.xml file. Replace the version tag with the specific version you require.

    <dependency>
      <groupId>org.clojure</groupId>
      <artifactId>core.cache</artifactId>
      <version>1.0.217</version>
    </dependency>