Skip to content
master
Switch branches/tags
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
dev
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Resilience4Clj Cache

CircleCI Clojars License Status

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).

Table of Contents

Getting Started

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).

Cache Settings

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:

  1. :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. Default true.
  2. :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:

  1. :provider-fn - function that receives the options map sent to create and must return a concrete implementation of a javax.cache.spi.CachingProvider. If :provider-fn is not specified, Resilience4clj will simply get the default caching provider on your clasppath.
  2. :manager-fn - function that receives the CachingProvider as a first argument and the options map sent to create as the second one and must return a concrete implementation of a CacheManager. If :manager-fn is not specified, Resilience4clj will simply ask the provider for its default CacheManager.
  3. :config-fn - function that receives the options maps sent to create and must return any concrete implementation of javax.cache.configuration.Configuration. If :config-fn is not specified, Resilience4clj will create a MutableConfiguration 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 be java.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}

Fallback Strategies

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:

  1. 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).
  2. 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).
  3. Advanced: multiple strategies can also be combined in order to create even better fallback strategies.

Manual Cache Manipulation

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:

  1. (put! <cache> <args> <value>): will put the <value> in <cache> keyed by <args>
  2. (get <cache> <args>): will get the cached value from <cache> keyed by <args>
  3. (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:

  1. You are not using any of the automatic, decorated features of the cache - therefore you've got no fallback for instance
  2. 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.
  3. The put! and get interfaces prefer dealing with <args> as a list. If you don send a seqable? 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.

Invalidating the Cache

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)

Using as an Effect

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.

Metrics

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.

Events

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:

  1. :HIT - informs that a call has hit the cache
  2. :MISSED - informs that a call has missed the cache
  3. :ERROR - informs that an error has taken place on the call
  4. :EXPIRED - informs that an entry has expired on the cache
  5. :MANUAL-PUT - informs that a manual put has happened
  6. :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.

Exception Handling

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.

Composing Further

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.

Bugs

If you find a bug, submit a Github issue.

Help

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.

License

Copyright © 2019 Tiago Luchini

Distributed under the MIT License.

About

Resilience4Clj Cache lets you decorate a function call with a distributed caching infrastructure as provided by any javax.cache provider.

Topics

Resources

License

Packages

No packages published