JBoss.orgCommunity Documentation

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:

:mode:local, :distributed in a clusterReplication mode. One of :local, :invalidated, :replicated or :distributed.
:synctrueWhether replication occurs synchronously during the write.
:persistnilEnables entries to persist across server restarts in a file-backed store.
:seednilA map of initial entries to replace the existing ones.
:lockingnilThe locking mode within a transaction. One of :optimistic or :pessimistic or nil to indicate no locking
:encoding:ednOne of :edn, :fressian, :json, or :none
:max-entries-1The maximum number of entries allowed in the cache. A negative value implies no limit.
:eviction:lirsThe eviction strategy. One of :lru, :lirs or :unordered
:ttl-1Time-To-Live. The maximum time the entry will live before being expired. A negative value disables time-to-live expiration.
:idle-1The time after which an entry will expire if not accessed. A negative value disables idle expiration.
:units:secondsThe units for :ttl and :idle. One of :days, :hours, :minutes, :seconds, :milliseconds, :microseconds or :nanoseconds
:confignilAn 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)))))
Immutant 1.1.3