Resilience4clj is a lightweight fault tolerance library set built on top of GitHub's Resilience4j and inspired by Netflix Hystrix. It was designed for Clojure and functional programming with composability in mind.
Read more about the motivation and details of Resilience4clj here.
Resilience4Clj Cache lets you decorate a function call with a
distributed caching infrastructure as provided by any javax.cache
(JSR107) provider. The resulting function will behave as an advanced
form of memoization (think of it as distributed memoization with
monitoring and metrics).
- Getting Started
- Cache Settings
- Fallback Strategies
- Manual Cache Manipulation
- Invalidating the Cache
- Using as an Effect
- Metrics
- Events
- Exception Handling
- Composing Further
- Bugs
- Help!
Add resilience4clj/resilience4clj-cache
as a dependency to your
deps.edn
file:
resilience4clj/resilience4clj-cache {:mvn/version "0.1.0"}
If you are using lein
instead, add it as a dependency to your
project.clj
file:
[resilience4clj/resilience4clj-cache "0.1.0"]
Resilience4clj cache does depends on a concrete implementation of a caching engine to the JSR107 interfaces. Therefore, in order to use Resilience4clj cache you need to choose a compatible caching engine.
This is a far from comprehensive list of options:
- Cache2k: simple embedded, in-memory cache system
- Ehcache: supports supports offheap storage and distributed, persistence via Terracotta
- Infinispan: embedded caching as well as advanced functionality such as transactions, events, querying, distributed processing, off-heap and geographical failover.
- Redisson: Redis Java client in-Memory data grid
- Apache Ignite: memory-centric distributed database, caching, and processing platform for transactional, analytical, and streaming workloads delivering in-memory speeds at petabyte scale
For this getting started let's use a simple embedded, in-memory cache
via Infinispan. Add it as a dependency to your deps.edn
file:
org.infinispan/infinispan-embedded {:mvn/version "9.1.7.Final"}
Or, if you are using lein
instead, add it as a dependency to your
project.clj
file:
[org.infinispan/infinispan-embedded "9.1.7.Final"]
Once both Resilience4clj cache and a concrete cache engine in place you can require the library:
(require '[resilience4clj-cache.core :as c])
Then create a cache calling the function create
:
(def cache (c/create "my-cache"))
Now you can decorate any function you have with the cache you just defined.
For the sake of this example, let's create a function that takes 1000ms to return:
(defn slow-hello []
(Thread/sleep 1000)
"Hello World!")
You can now create a decorated version of your slow-hello
function
above combining the cache
we created before like this:
(def protected (c/decorate slow-hello cache))
When you call protected
for the first time it will take around
1000ms to run because of the timeout we added there. Subsequent calls
will return virtually instanteneaously because the return of the
function has been cached in memory.
(time (protected))
"Elapsed time: 1001.462526 msecs"
Hello World!
(time (protected))
"Elapsed time: 1.238522 msecs"
Hello World!
By default the create
function will set your cache as eternal so
every single call to protected
above will return "Hello World!"
for long as the cache entry is in memory (or until the cache is
manually invalidated - see function invalidate!
below).
If you simply call the create
function providing a cache name,
Resilience4clj cache will capture the default caching provider from
your classpath and then use sensible and simple settings to bring your
cache system up. These steps should cover many of the basic caching
scenarios.
The create
supports a second map argument for further
configurations.
There are two very basic fine-tuning settings available:
:eternal?
- whether this cache will retain its entries forever or not. Caching engines might still discard entries if certain conditions are met (i.e. full memory) so this should be used as an indication of intent more than a solid dependency. Defaulttrue
.:expire-after
- if you don't want an eternal cache entry, chances are you would prefer entries that expire after a certain amount of time. You can specify any amount of milliseconds of at least 1000 or higher (if specified,:eternal?
is automatically turned off).
For more advanced scenarios, you might want to set up your caching engine with all sorts of whistles and belts. In these scenarios you will need to provide a combination of factory functions to cover for your particular need:
:provider-fn
- function that receives the options map sent tocreate
and must return a concrete implementation of ajavax.cache.spi.CachingProvider
. If:provider-fn
is not specified, Resilience4clj will simply get the default caching provider on your clasppath.:manager-fn
- function that receives theCachingProvider
as a first argument and the options map sent tocreate
as the second one and must return a concrete implementation of aCacheManager
. If:manager-fn
is not specified, Resilience4clj will simply ask the provider for its defaultCacheManager
.:config-fn
- function that receives the options maps sent tocreate
and must return any concrete implementation ofjavax.cache.configuration.Configuration
. If:config-fn
is not specified, Resilience4clj will create aMutableConfiguration
and use:eternal?
and:expire-after
above to do some basic fine tuning on the config.
Things to notice when setting up your cache using these factory functions above:
- Among many other impactful settings, your expiration policies will definitely affect the way that your cache behaves.
- If your configuration does not expose mutable abilities such as the
method
registerCacheEntryListener
, then listening the expiration events as documented in the Events section is not going to work. - Resilience4clj cache expects the
<K, V>
of the Cache to bejava.lang.String, java.lang.Object
. Other settings have not been tested and might not work.
Here is an example creating a cache that expires in a minute:
(def cache (c/create {:expire-after 60000}))
The function config
returns the configuration of a cache in case
you need to inspect it. Example:
(c/config cache)
=> {:provider-fn #object[resilience4clj-cache.core$get-provider...
:manager-fn #object[resilience4clj-cache.core$get-manager...
:config-fn #object[resilience4clj-cache.core$get-config...
:eternal? true
:expire-after nil}
When decorating your function with a cache you can opt to have a fallback function. This function will be called instead of an exception being thrown when the call would fail (its traditional throw). This feature can be seen as an obfuscation of a try/catch to consumers.
This is particularly useful if you want to obfuscate from consumers that the external dependency failed. Example:
(def cache (c/create "my-cache"))
(defn hello [person]
;; hypothetical flaky, external HTTP request
(str "Hello " person))
(def cached-hello
(c/decorate hello
{:fallback (fn [e person]
(str "Hello from fallback to " person))}))
The signature of the fallback function is the same as the original
function plus an exception as the first argument (e
on the example
above). This exception is an ExceptionInfo
wrapping around the real
cause of the error. You can inspect the :cause
node of this
exception to learn about the inner exception:
(defn fallback-fn [e]
(str "The cause is " (-> e :cause)))
For more details on Exception Handling see the section below.
When considering fallback strategies there are usually three major strategies:
- Failure: the default way for Resilience4clj - just let the exceptiohn flow - is called a "Fail Fast" approach (the call will fail fast once the breaker is open). Another approach is "Fail Silently". In this approach the fallback function would simply hide the exception from the consumer (something that can also be done conditionally).
- Content Fallback: some of the examples of content fallback are returning "static content" (where a failure would always yield the same static content), "stubbed content" (where a failure would yield some kind of related content based on the paramaters of the call), or "cached" (where a cached copy of a previous call with the same parameters could be sent back).
- Advanced: multiple strategies can also be combined in order to create even better fallback strategies.
By default Resilience4clj cache can be used as a decorator to your external calls and it will take care of basic caching for you. In some circumstances though you might want to interact directly with its cache. One such situation is when using the cache as an effect.
There are three functions to directly manipulate the cache:
(put! <cache> <args> <value>)
: will put the<value>
in<cache>
keyed by<args>
(get <cache> <args>)
: will get the cached value from<cache>
keyed by<args>
(contains? <cache> <args>)
: convenience check whether the entry keyed by<args>
is in the<cache>
<args>
can be any Clojure object that supports .toString
.
Caveats when manually using the cache:
- You are not using any of the automatic, decorated features of the cache - therefore you've got no fallback for instance
- Resilience4clj cache internally segments the cache for every function that it decorates and every combination of arguments sent to the function. When used manually, only one "caching space" is used. Therefore, if the same args are used in different places with different semantic meanings you will still get the same values from the cache.
- The
put!
andget
interfaces prefer dealing with<args>
as a list. If you don send aseqable?
as<args>
, whatever parameter you send will be transformed into a list. Therefore (due to the bullet above) sending:foobar
is equivalent to'(:foobar)
See using the cache as an effect for a use case where direct manipulation of the cache is very useful.
By default Resilience4clj cache uses an eternal cache (this can be set up differently if you want) therefore, you might eventually want to invalidate the cache altogether.
In order to do so, use the function invalidate!
. In the following
code, the cache
will be invalidated:
(c/invalidate! cache)
Resilience4clj cache is a great alternative for creating fallback strategies in conjunction with other Resilience4clj libraries.
Some libraries like Resilience4clj retry and Resilience4clj circuit breaker have a feature called effects for capturing side-effects. In this context, a side-effect is a handler for processing the successful output of the decorated function call.
For instance, assuming that you have required
resilience4clj-retry.core
as r
and resilience4clj-retry.core
as
c
:
(def retry (r/create "hello-retry"))
(def cache (c/create "hello-cache"))
(defn hello [person]
;; hypothetical flaky, external HTTP request
(str "Hello " person "!!"))
Now that you have a default retry
, a default cache
, and a
potentially flaky function hello
let's create an effect that puts
the returned value in the cache, a fallback that gets it from the
cache and a decorated function that puts them together:
(defn effect-fn
[ret person]
(c/put! cache person ret))
(defn fallback-fn
[e person]
(c/get cache person))
(def safe-cached-hello
(r/decorate hello retry
{:effect effect-fn
:fallback fallback-fn}))
The behavior here is that when calling the safe-cached-hello
function, the function hello
will be retried for a few times (max
default is 3). In case of success, the returned value will be put in
the cache. In case of failure whatever value is on the cache will be
returned.
Of course, this is a very naive approach as it will simply return nil
in
a failure scenario where the cache is empty. A more advanced approach
would be:
(defn fallback-fn
[e person]
(if (c/contains? cache person)
(c/get cache person)
(throw e)))
In the example above, if the the entry for person
is still not in
place, the underlying expression is thrown.
By combining several modules from Resilience4clj (see the list here) you can achieve very advanced behavior quickly. For instance:
(def very-safe-hello
(-> hello
(r/decorate retry {:effect effect-fn
:fallback fallback-fn})
(tl/decorate timelimiter)
(cb/decorate breaker)))
With the snippet above, you are retrying hello
in case of failure,
caching its return when succesful, having a cached fallback strategy,
within a pre-defined time limit (execution budget) and protected by a
ring-based circuit breaker.
All that in just 5 lines.
The function metrics
returns a map with the metrics of the cache:
(c/metrics cache)
=> {:hits 0
:misses 0
:errors 0
:manual-puts 0
:manual-gets 0}
The nodes should be self-explanatory. Because direct manipulation of the cache does not go through the automatic hit/miss logic, these are kept separatelly.
The metrics can be reset with a call to the reset!
function:
(c/reset! cache)
Metrics will cycle back to 0 when they reach Long/MAX_VALUE
.
You can listen to events generated by the use of the cache. This is particularly useful for logging, debugging, or monitoring the health of your cache.
(def cache (c/create "my-cache"))
(c/listen-event cache
:HIT
(fn [evt]
(println (str "Your cache has been hit"))))
There are six types of events:
:HIT
- informs that a call has hit the cache:MISSED
- informs that a call has missed the cache:ERROR
- informs that an error has taken place on the call:EXPIRED
- informs that an entry has expired on the cache:MANUAL-PUT
- informs that a manual put has happened:MANUAL-GET
- informs that a manual get has happened
Notice you have to listen to a particular type of event by specifying the event-type you want to listen.
Note on :EXPIRED
: expiration rules differ from caching provider
to caching provider. They might also differ depending on the way you
have set up your cache (see cache settings for more
details). In every practical sense, you should not rely on :EXPIRED
for anything business critical unless the behavior of your
cache/settings is known and consistent.
All events receive a map containing the :event-type
, the
:cache-name
, the event :creation-time
, the function name that
generated the entry :fn-name
, and the internal :key
that
represents the unique id of the cache entry. For now, :EXPIRED
does
not support :fn-name
.
When using the fallback function, be aware that its signature is the
same as the original function plus an exception (e
on the example
above). This exception is an ExceptionInfo
wrapping around the real
cause of the error. You can inspect the :cause
node of this
exception to learn about the inner exception:
If you are not using a fallback function, then you don't need to worry about anything. Your exception will bubble up as you would expect.
Resilience4clj is composed of several modules that easily compose together. For instance, if you are also using the retry module and assuming your import and basic settings look like this:
(ns my-app
(:require [resilience4clj-cache.core :as c]
[resilience4clj-retry.core :as r]))
;; create a retry with default settings
(def retry (r/create "my-retry"))
;; create cache with default settings
(def cache (c/create "my-cache"))
;; flaky function you want to potentially retry
(defn flaky-hello []
;; hypothetical request to a flaky server that might fail (or not)
"Hello World!")
Then you can create a protected call that combines both the retry and the cache:
(def protected-hello (-> flaky-hello
(r/decorate retry)
(c/decorate cache)))
The resulting function protected-hello
will retry before persisting
to cache and skip retries altogether in case of cache hits as you
would expect. The composing order makes a big difference of course, if
retry and cache had been reversed here, flaky-hello
would cache
first and the retry would wrap the cache which is not what you would
want.
The cache module is special as it composes very nicely with a effect/fallback strategy. See how to use cache as an effect.
If you find a bug, submit a Github issue.
This project is looking for team members who can help this project succeed! If you are interested in becoming a team member please open an issue.
Copyright © 2019 Tiago Luchini
Distributed under the MIT License.