This tutorial is for Immutant 1.x. Go here for the 2.x documentation!

In this tutorial, we'll explore some of Immutant's caching features. For detailed documentation, see the caching chapter of the Immutant manual.

JBoss AS7 -- and therefore, Immutant -- comes with the Infinispan data grid baked right in, obviating the need to manage a separate caching service like Memcached for your applications.

Infinispan is a state-of-the-art, high-speed, low-latency, distributed data grid. It is capable of efficiently replicating key-value stores -- essentially souped-up ConcurrentMap implementations -- across a cluster. But it can also serve as a capable in-memory data cache, too: providing features such as write-through/write-behind persistence, multiple eviction policies, and transactions.

Creating a cache

Like most databases and caches, Immutant's InfinispanCache is mutable. Infinispan uses many of the same techniques as Clojure itself, e.g. MVCC, to provide "sane data management", enabling fast reads of data that may have been put there by another -- possibly remote -- process.

Caches are defined using the immutant.cache/create function. Its only required argument is a name. Creating two caches with the same name means each is backed by the same Infinispan cache, and the second call to create will cause the first to restart, losing all its non-durable entries. It's usually best to call immutant.cache/lookup-or-create instead, as it will only create the cache if it doesn't already exist.

Options that determine clustering behavior and entry lifespan are provided as well.

(require '[immutant.cache :as cache])

;; Define a cache named 'bob' whose entries will automatically expire
;; if either a) 10 minutes elapses since it was written or b) 1 minute
;; elapses since it was last accessed
(def c (cache/create "bob" :ttl 10, :idle 1, :units :minutes))

Writing to a cache

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 hash of lifespan-oriented parameters (:ttl :idle :units) that may be used to override the values specified when the cache was created.

(require '[immutant.cache :as cache])

;;; Put an entry in the cache
(cache/put c :a 1)

;;; Override its time-to-live
(cache/put c :a 1 {:ttl 1, :units :hours})
;;; optional syntax...
(cache/put c :a 1 {:ttl [1 :hour]})

;;; Add all the entries in the map to the cache
(cache/put-all c {:b 2, :c 3})

;;; Put it in only if key is not already present
(cache/put-if-absent c :b 6)                  ;=> 2
(cache/put-if-absent c :d 4)                  ;=> nil

;;; Put it in only if key is already present
(cache/put-if-present c :e 5)                 ;=> nil
(cache/put-if-present c :b 6)                 ;=> 2

;;; Put it in only if key is there and current matches old
(cache/put-if-replace c :b 2 0)               ;=> false
(cache/put-if-replace c :b 6 0)               ;=> true
(:b c)                                        ;=> 0

Reading from a cache

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 (cache/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

Memoization

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.

Standards for caching and memoization in Clojure are emerging in the form of core.cache and core.memoize, respectively. Because the InfinispanCache implements clojure.core.cache/CacheProtocol it can act as an underlying implementation for clojure.core.memoize/PluggableMemoization. Immutant includes a higher-order memo function for doing exactly that:

(immutant.cache/memo a-slow-function "a name")

An Example

We'll create a simple web app with a single request to which we'll pass an integer. The request handler will pass that number to a very slow increment function: it'll sleep for that number of seconds before returning its increment. For us, this sleepy function represents a particularly time-consuming operation that will benefit from memoization.

Of course we'll need a project.clj

(defproject example "1.0.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.4.0"]])

Next, the Immutant application bootstrap file, src/immutant/init.clj, into which we'll put all our code for this example.

(ns immutant.init
  (:use [ring.util.response]
        [ring.middleware.params])
  (:require [immutant.cache :as cache]
            [immutant.web :as web]))

;; Our slow function
(defn slow-inc [t]
  (Thread/sleep (* t 1000))
  (inc t))

;; Our memoized version of the slow function
(def memoized-inc (cache/memo slow-inc "sleepy"))

;; Our Ring handler
(defn handler [{params :params}]
  (let [t (Integer. (get params "t" 1))]
    (response (str "value=" (memoized-inc t) "\n"))))

;; Start up our web app
(web/start "/" (wrap-params #'handler))

Make sure you have a recent version of Immutant:

$ lein immutant install

And cd to the directory containing the above two files and deploy your app:

$ lein immutant deploy

Now run an Immutant in one shell:

$ lein immutant run

In another shell, try:

$ curl "http://localhost:8080/example/?t=5"

With any luck, that should return 6 after about 5 seconds. Now run it again and it should return 6 immediately.

Now fire off a request with t=20 or so, and wait a few seconds, but before it completes hit it again with the same t value. You'll notice that the second request will not have to sleep for the full 20 seconds; it returns immediately after the first completes.

Immutant caching really shines in a cluster, taking advantage of the Infinispan "data grid" features, but the API doesn't change, whether your app is deployed to a cluster or not.