Chapter 9. Immutant Caching
9.1. Introduction
Immutant provides built-in support for local and distributed caching through Infinispan. And because Immutant caches implement clojure.core.cache/CacheProtocol, they may be used as standard memoization stores.
9.1.1. Infinispan
Immutant encapsulates the JBoss Infinispan data grid, enabling simple construction of transactional, high-performance key/value stores that can either be run locally or efficiently replicated across a cluster.
Infinispan offers a number of clustering modes that determine how data is replicated when an entry is written to the cache.
9.2. Caching Modes
Infinispan supports three clustered modes and one non-clustered.
9.2.1. Local
This is the only supported mode when Immutant runs non-clustered: essentially an enhanced, in-memory ConcurrentMap implementation, featuring write-through/write-behind persistence, eviction, expiration, JTA/XA support, MVCC-based concurency, and JMX manageability.
9.2.2. Invalidated
No data is actually shared among the cluster nodes in this mode. Instead, notifications are sent to all nodes when data changes, causing them to evict their stale copies of the updated entry.
9.2.3. Replicated
In this mode, entries added to any cache instance will be copied to all other cache instances in the cluster, and can then be retrieved locally from any instance. This mode is probably impractical for clusters of any significant size. Infinispan recommends 10 as a reasonable upper bound on the number of replicated nodes.
9.2.4. Distributed
This is the default mode when Immutant runs clustered. This mode enables Infinispan clusters to achieve "linear scalability". Cache entries are copied to a fixed number of cluster nodes (2, by default) regardless of the cluster size. Distribution uses a consistent hashing algorithm to determine which nodes will store a given entry.
9.3. The API
9.3.1. Creating
All Infinispan caches have a name scoped to the app server (or
cluster) hence the names are global for all practical purposes.
Caches are created using the immutant.cache/create function. Its
only required argument is a name. Calling create
for an existing
cache restarts it, clearing all its data.
Additional options may be passed as in-line keyword arguments. The following are supported:
Option | Default | Description |
---|---|---|
:mode | :local , :distributed in a cluster | Replication mode. One of :local , :invalidated , :replicated or :distributed . |
:sync | true | Whether replication occurs synchronously during the write. |
:persist | nil | Enables entries to persist across server restarts in a file-backed store. |
:seed | nil | A map of initial entries to replace the existing ones. |
:locking | nil | The locking mode within a transaction. One of :optimistic or :pessimistic or nil to indicate no locking |
:encoding | :edn | One of :edn , :fressian , :json , or :none |
:max-entries | -1 | The maximum number of entries allowed in the cache. A negative value implies no limit. |
:eviction | :lirs | The eviction strategy. One of :lru , :lirs or :unordered |
:ttl | -1 | Time-To-Live. The maximum time the entry will live before being expired. A negative value disables time-to-live expiration. |
:idle | -1 | The time after which an entry will expire if not accessed. A negative value disables idle expiration. |
:units | :seconds | The units for :ttl and :idle . One of :days , :hours , :minutes , :seconds , :milliseconds , :microseconds or :nanoseconds |
:config | nil | An Infinispan Configuration instance. See advanced configuration below. |
When not clustered, the value of :mode
is ignored and set to
:local
.
If :persist
is true
(or any non-nil, non-string value), cache
entries will persist in the current directory. Override this by
setting :persist
to a string naming the desired directory, the
parents of which will be created, if necessary.
Seeding a cache will cause existing entries to be deleted, a potentially expensive operation depending on replication mode.
With an optimistic locking mode, locks are obtained at transaction prepare time and released at commit or rollback. Pessimistic transactions obtain locks on keys at the time the key is written, releasing at commit/rollback. See the Infinispan docs for more details.
The :units
option applies to both :idle
and :ttl
, but to
achieve finer granularity you may alternatively pass a two element
vector containing the amount and units, e.g.
(cache/create "my-cache" :ttl [42 :days] :idle [42 :minutes])
Some more examples are in order:
(ns example.test (:use [immutant.cache])) ;; Obtain a cache in :distributed mode if clustered, :local otherwise (def c1 (create "jimi")) ;; A cache in :invalidated mode if clustered, :local otherwise (def c2 (create "jeff" :mode :invalidated)) ;; Initialize a replicated cache, if clustered, with a seed (def c3 (create "billy" :mode :replicated, :seed {:a 1 :b 2})) ;; Expire all entries after 10 minutes and any not accessed after 1 minute (def c4 (create "jerry" :ttl 10, :idle 1, :units :minutes))
Often, you'll share caches with other applications or services
running in the same Immutant or cluster. The immutant.cache/lookup
function is provided to obtain a reference to an existing cache
without having to recreate it and clear all its data. If the cache
doesn't exist, nil is returned. There is also a convenience
function, immutant.cache/lookup-or-create that will only create the
cache if it doesn't already exist. It accepts the same options as
create
.
9.3.2. Writing
Immutant caches are mutable. This is sensible in a clustered environment, because the local process benefits from fast reads of data that may have been put there by a remote process. We effectively shift the responsibility of "sane data management", i.e. MVCC, from Clojure to Infinispan.
Immutant caches implement the immutant.cache/Mutable protocol, through which Infinispan's cache manipulation features are exposed.
Data is inserted into an Immutant cache using one of the put
functions of the Mutable
protocol. Each takes an optional map of
lifespan-oriented parameters (:ttl
:idle
:units
) that may be
used to override the values specified when the cache was created.
(def c (create "foo" :ttl 300)) ;;; Put an entry in the cache (put c :a 1) ;;; Override its time-to-live (put c :a 1 {:ttl [1 :hour]}) ;;; Add all the entries in the map to the cache (put-all c {:b 2, :c 3}) ;;; Put it in only if key is not already present (put-if-absent c :b 6) ;=> 2 (put-if-absent c :d 4) ;=> nil ;;; Put it in only if key is already present (put-if-present c :e 5) ;=> nil (put-if-present c :b 6) ;=> 2 ;;; Put it in only if key is there and current matches old (put-if-replace c :b 2 0) ;=> false (put-if-replace c :b 6 0) ;=> true (:b c) ;=> 0
The conditional put-if-*
functions are atomic and return values
as specified by java.util.concurrent.ConcurrentMap, so
put-if-absent
returns the previously mapped value on failure, and
put-if-present
returns the previously mapped value on success.
To remove entries from the cache, use delete
.
(def c (create "bar" :seed {:a 1 :b 2})) ;;; Deleting a missing key is harmless (delete c :missing) ;=> nil ;;; Deleting an existing key returns its value (delete c :b) ;=> 2 ;;; If value is passed, both must match for delete to succeed (delete c :a 2) ;=> false (delete c :a 1) ;=> true ;;; Clear all keys, returning the empty cache (delete-all c) ;=> c
9.3.3. Reading
Data is read from an Immutant cache the same way data is read from any standard Clojure map, i.e. using core Clojure functions.
(def c (create "baz" :seed {:a 1, :b {:c 3, :d 4}})) ;;; Use get to obtain associated values (get c :a) ;=> 1 (get c :x) ;=> nil (get c :x 42) ;=> 42 ;;; Symbols look up their value (:a c) ;=> 1 (:x c 42) ;=> 42 ;;; Nested structures work as you would expect (get-in c [:b :c]) ;=> 3 ;;; Use find to return entries (find c :a) ;=> [:a 1] ;;; Use contains? to check membership (contains? c :a) ;=> true (contains? c :x) ;=> false
9.3.4. Memoizing
Memoization is an optimization technique associating a cache of calculated values with a potentially expensive function, incurring the expense only once, with subsequent calls retrieving the result from the cache. The keys of the cache are the arguments passed to the function.
Because an Immutant cache implements clojure.core.cache/CacheProtocol, it can act as an underlying implementation for clojure.core.memoize/PluggableMemoization. Immutant includes a higher-order immutant.cache/memo function for doing exactly that:
(require '[immutant.cache :as ic]) ;;; Other than the function to be memoized, arguments are the same as ;;; for the cache function. (def memoized-fn (ic/memo slow-fn "foo" :mode :distributed, :ttl 600)) ;;; Invoking the memoized function fills the cache with the result ;;; from the slow function the first time it is called. (memoized-fn 1 2 3) ;=> 42 ;;; Subsequent invocations with the same parameters return the result ;;; from the cache, avoiding the overhead of the slow function (memoized-fn 1 2 3) ;=> 42 ;;; It's possible to manipulate the cache backing the memoized ;;; function by referring to its name (def c (ic/create "foo")) (get c [1 2 3]) ;=> 42
9.3.5. Advanced Infinispan Configuration
The immutant.cache
namespace only exposes a subset of an
Infinispan cache's configurable parameters. This is by design, in
the spirit of convention over configuration, but more complex
configuration is still possible via Clojure's Java interop.
Infinispan's "fluent API" is invoked via a ConfigurationBuilder as
described in their docs for configuring a cache programmatically. A
ConfigurationBuilder
instance may be obtained by calling
immutant.cache.core/builder, passing the same options you would
when creating a cache. This returns a builder initialized from the
Immutant defaults and the options you pass in. You can then tweak
that builder however you like before calling its build
method to
return an Infinispan Configuration instance you can then pass to
immutant.cache/create
.
IMPORTANT: You should always pass the same options to create
that you passed to builder
, in addition to the :config
option,
because some of those options, e.g. :encoding
and the
lifespan-oriented ones, are not a part of the Configuration
interface. For those that do potentially conflict, the :config
option will always take precedence.
For example, let's say you always want your caches to use
synchronization when they're involved in a transaction. By default,
they participate as an XAResource
to avoid a potential race
condition, but your app may be immune to that condition, so to
avoid a slight performance penalty, you might create your caches
using the following function.
(require 'immutant.cache '[immutant.cache.core :refer [builder]] '[immutant.util :refer [mapply]]) (defn your-create [name & {:as opts}] (let [builder (builder opts)] (.. builder transaction (useSynchronization true)) (mapply immutant.cache/create name (assoc opts :config (.build builder)))))