{"id":18395782,"url":"https://github.com/resilience4clj/resilience4clj-cache","last_synced_at":"2025-12-12T01:14:57.156Z","repository":{"id":62434475,"uuid":"160754481","full_name":"resilience4clj/resilience4clj-cache","owner":"resilience4clj","description":"Resilience4Clj Cache lets you decorate a function call with a distributed caching infrastructure as provided by any javax.cache provider.","archived":false,"fork":false,"pushed_at":"2019-07-25T15:20:27.000Z","size":49,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-02T04:04:44.036Z","etag":null,"topics":["cache","circuit-breaker","clojure","metrics","resilience","retry"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/resilience4clj.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-12-07T01:32:30.000Z","updated_at":"2023-03-12T23:42:05.000Z","dependencies_parsed_at":"2022-11-01T21:01:46.026Z","dependency_job_id":null,"html_url":"https://github.com/resilience4clj/resilience4clj-cache","commit_stats":null,"previous_names":["luchiniatwork/resilience4clj-cache"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/resilience4clj%2Fresilience4clj-cache","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/resilience4clj%2Fresilience4clj-cache/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/resilience4clj%2Fresilience4clj-cache/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/resilience4clj%2Fresilience4clj-cache/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/resilience4clj","download_url":"https://codeload.github.com/resilience4clj/resilience4clj-cache/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247589793,"owners_count":20963022,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cache","circuit-breaker","clojure","metrics","resilience","retry"],"created_at":"2024-11-06T02:11:55.570Z","updated_at":"2025-12-12T01:14:52.130Z","avatar_url":"https://github.com/resilience4clj.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"[about]: https://github.com/resilience4clj/resilience4clj-circuitbreaker/blob/master/docs/ABOUT.md\n[breaker]: https://github.com/resilience4clj/resilience4clj-circuitbreaker/\n[circleci-badge]: https://circleci.com/gh/resilience4clj/resilience4clj-cache.svg?style=shield\u0026circle-token=e366fa2eedef2d2163ec81cc00d315137bc0adb2\n[circleci]: https://circleci.com/gh/resilience4clj/resilience4clj-cache\n[clojars-badge]: https://img.shields.io/clojars/v/resilience4clj/resilience4clj-cache.svg\n[clojars]: http://clojars.org/resilience4clj/resilience4clj-cache\n[github-issues]: https://github.com/resilience4clj/resilience4clj-cache/issues\n[license-badge]: https://img.shields.io/badge/license-MIT-blue.svg\n[license]: ./LICENSE\n[retry]: https://github.com/resilience4clj/resilience4clj-retry/\n[status-badge]: https://img.shields.io/badge/project%20status-alpha-brightgreen.svg\n\n# Resilience4Clj Cache\n\n[![CircleCI][circleci-badge]][circleci]\n[![Clojars][clojars-badge]][clojars]\n[![License][license-badge]][license]\n![Status][status-badge]\n\nResilience4clj is a lightweight fault tolerance library set built on\ntop of GitHub's Resilience4j and inspired by Netflix Hystrix. It was\ndesigned for Clojure and functional programming with composability in\nmind.\n\nRead more about the [motivation and details of Resilience4clj\nhere][about].\n\nResilience4Clj Cache lets you decorate a function call with a\ndistributed caching infrastructure as provided by any `javax.cache`\n(JSR107) provider. The resulting function will behave as an advanced\nform of memoization (think of it as distributed memoization with\nmonitoring and metrics).\n\n## Table of Contents\n\n* [Getting Started](#getting-started)\n* [Cache Settings](#cache-settings)\n* [Fallback Strategies](#fallback-strategies)\n* [Manual Cache Manipulation](#manual-cache-manipulation)\n* [Invalidating the Cache](#invalidating-the-cache)\n* [Using as an Effect](#using-as-an-effect)\n* [Metrics](#metrics)\n* [Events](#events)\n* [Exception Handling](#exception-handling)\n* [Composing Further](#composing-further)\n* [Bugs](#bugs)\n* [Help!](#help)\n\n## Getting Started\n\nAdd `resilience4clj/resilience4clj-cache` as a dependency to your\n`deps.edn` file:\n\n``` clojure\nresilience4clj/resilience4clj-cache {:mvn/version \"0.1.0\"}\n```\n\nIf you are using `lein` instead, add it as a dependency to your\n`project.clj` file:\n\n``` clojure\n[resilience4clj/resilience4clj-cache \"0.1.0\"]\n```\n\nResilience4clj cache does depends on a concrete implementation of a\ncaching engine to the JSR107 interfaces. Therefore, in order to use\nResilience4clj cache you need to choose a compatible caching engine.\n\nThis is a far from comprehensive list of options:\n\n- [Cache2k](https://cache2k.org): simple embedded, in-memory cache system\n- [Ehcache](http://www.ehcache.org): supports supports offheap storage\n  and distributed, persistence via Terracotta\n- [Infinispan](http://infinispan.org/): embedded caching as well as\n  advanced functionality such as transactions, events, querying,\n  distributed processing, off-heap and geographical failover.\n- [Redisson](https://redisson.org): Redis Java client in-Memory data\n  grid\n- [Apache Ignite](https://ignite.apache.org): memory-centric\n  distributed database, caching, and processing platform for\n  transactional, analytical, and streaming workloads delivering\n  in-memory speeds at petabyte scale\n\nFor this getting started let's use a simple embedded, in-memory cache\nvia Infinispan. Add it as a dependency to your `deps.edn` file:\n\n``` clojure\norg.infinispan/infinispan-embedded {:mvn/version \"9.1.7.Final\"}\n```\n\nOr, if you  are using `lein` instead,  add it as a  dependency to your\n`project.clj` file:\n\n``` clojure\n[org.infinispan/infinispan-embedded \"9.1.7.Final\"]\n```\n\nOnce both Resilience4clj cache and a concrete cache engine in place\nyou can require the library:\n\n``` clojure\n(require '[resilience4clj-cache.core :as c])\n```\n\nThen create a cache calling the function `create`:\n\n``` clojure\n(def cache (c/create \"my-cache\"))\n```\n\nNow you can decorate any function you have with the cache you just\ndefined.\n\nFor the sake of this example, let's create a function that takes\n1000ms to return:\n\n``` clojure\n(defn slow-hello []\n  (Thread/sleep 1000)\n  \"Hello World!\")\n```\n\nYou can now create a decorated version of your `slow-hello` function\nabove combining the `cache` we created before like this:\n\n``` clojure\n(def protected (c/decorate slow-hello cache))\n```\n\nWhen you call `protected` for the first time it will take around\n1000ms to run because of the timeout we added there. Subsequent calls\nwill return virtually instanteneaously because the return of the\nfunction has been cached in memory.\n\n``` clojure\n(time (protected))\n\"Elapsed time: 1001.462526 msecs\"\nHello World!\n\n(time (protected))\n\"Elapsed time: 1.238522 msecs\"\nHello World!\n```\n\nBy default the `create` function will set your cache as eternal so\nevery single call to `protected` above will return `\"Hello World!\"`\nfor long as the cache entry is in memory (or until the cache is\nmanually invalidated - see function `invalidate!` below).\n\n## Cache Settings\n\nIf you simply call the `create` function providing a cache name,\nResilience4clj cache will capture the default caching provider from\nyour classpath and then use sensible and simple settings to bring your\ncache system up. These steps should cover many of the basic caching\nscenarios.\n\nThe `create` supports a second map argument for further\nconfigurations.\n\nThere are two very basic fine-tuning settings available:\n\n1. `:eternal?` - whether this cache will retain its entries forever or\n   not. Caching engines might still discard entries if certain\n   conditions are met (i.e. full memory) so this should be used as an\n   indication of intent more than a solid dependency. Default `true`.\n2. `:expire-after` - if you don't want an eternal cache entry, chances\n   are you would prefer entries that expire after a certain amount of\n   time. You can specify any amount of milliseconds of at least 1000\n   or higher (if specified, `:eternal?` is automatically turned off).\n\nFor more advanced scenarios, you might want to set up your caching\nengine with all sorts of whistles and belts. In these scenarios you\nwill need to provide a combination of factory functions to cover for\nyour particular need:\n\n1. `:provider-fn` - function that receives the options map sent to\n   `create` and must return a concrete implementation of a\n   `javax.cache.spi.CachingProvider`. If `:provider-fn` is not\n   specified, Resilience4clj will simply get the default caching\n   provider on your clasppath.\n2. `:manager-fn` - function that receives the `CachingProvider` as a\n   first argument and the options map sent to `create` as the second\n   one and must return a concrete implementation of a\n   `CacheManager`. If `:manager-fn` is not specified, Resilience4clj\n   will simply ask the provider for its default `CacheManager`.\n3. `:config-fn` - function that receives the options maps sent to\n   `create` and must return any concrete implementation of\n   `javax.cache.configuration.Configuration`. If `:config-fn` is not\n   specified, Resilience4clj will create a `MutableConfiguration` and\n   use `:eternal?` and `:expire-after` above to do some basic fine\n   tuning on the config.\n\nThings to notice when setting up your cache using these factory\nfunctions above:\n\n* Among many other impactful settings, your expiration policies will\n  definitely affect the way that your cache behaves.\n* If your configuration does not expose mutable abilities such as the\n  method `registerCacheEntryListener`, then listening the expiration\n  events as documented in the [Events](#events) section is not going\n  to work.\n* Resilience4clj cache expects the `\u003cK, V\u003e` of the Cache to be\n  `java.lang.String, java.lang.Object`. Other settings have not been\n  tested and might not work.\n\nHere is an example creating a cache that expires in a minute:\n\n``` clojure\n(def cache (c/create {:expire-after 60000}))\n```\n\nThe function `config` returns the configuration of a cache in case\nyou need to inspect it. Example:\n\n``` clojure\n(c/config cache)\n=\u003e {:provider-fn #object[resilience4clj-cache.core$get-provider...\n    :manager-fn #object[resilience4clj-cache.core$get-manager...\n    :config-fn #object[resilience4clj-cache.core$get-config...\n    :eternal? true\n    :expire-after nil}\n```\n\n## Fallback Strategies\n\nWhen decorating your function with a cache you can opt to have a\nfallback function. This function will be called instead of an\nexception being thrown when the call would fail (its traditional\nthrow). This feature can be seen as an obfuscation of a try/catch to\nconsumers.\n\nThis is particularly useful if you want to obfuscate from consumers\nthat the external dependency failed. Example:\n\n``` clojure\n(def cache (c/create \"my-cache\"))\n\n(defn hello [person]\n  ;; hypothetical flaky, external HTTP request\n  (str \"Hello \" person))\n\n(def cached-hello\n  (c/decorate hello\n              {:fallback (fn [e person]\n                           (str \"Hello from fallback to \" person))}))\n```\n\nThe signature of the fallback function is the same as the original\nfunction plus an exception as the first argument (`e` on the example\nabove). This exception is an `ExceptionInfo` wrapping around the real\ncause of the error. You can inspect the `:cause` node of this\nexception to learn about the inner exception:\n\n``` clojure\n(defn fallback-fn [e]\n  (str \"The cause is \" (-\u003e e :cause)))\n```\n\nFor more details on [Exception Handling](#exception-handling) see the\nsection below.\n\nWhen considering fallback strategies there are usually three major\nstrategies:\n\n1. **Failure**: the default way for Resilience4clj - just let the\n   exceptiohn flow - is called a \"Fail Fast\" approach (the call will\n   fail fast once the breaker is open). Another approach is \"Fail\n   Silently\". In this approach the fallback function would simply hide\n   the exception from the consumer (something that can also be done\n   conditionally).\n2. **Content Fallback**: some of the examples of content fallback are\n   returning \"static content\" (where a failure would always yield the\n   same static content), \"stubbed content\" (where a failure would\n   yield some kind of related content based on the paramaters of the\n   call), or \"cached\" (where a cached copy of a previous call with the\n   same parameters could be sent back).\n3. **Advanced**: multiple strategies can also be combined in order to\n   create even better fallback strategies.\n\n## Manual Cache Manipulation\n\nBy default Resilience4clj cache can be used as a decorator to your\nexternal calls and it will take care of basic caching for you. In some\ncircumstances though you might want to interact directly with its\ncache. One such situation is when [using the cache as an\neffect](#using-as-an-effect).\n\nThere are three functions to directly manipulate the cache:\n\n1. `(put! \u003ccache\u003e \u003cargs\u003e \u003cvalue\u003e)`: will put the `\u003cvalue\u003e` in\n   `\u003ccache\u003e` keyed by `\u003cargs\u003e`\n2. `(get \u003ccache\u003e \u003cargs\u003e)`: will get the cached value from `\u003ccache\u003e`\n   keyed by `\u003cargs\u003e`\n3. `(contains? \u003ccache\u003e \u003cargs\u003e)`: convenience check whether the entry\n   keyed by `\u003cargs\u003e` is in the `\u003ccache\u003e`\n\n`\u003cargs\u003e` can be any Clojure object that supports `.toString`.\n\nCaveats when manually using the cache:\n\n1. You are not using any of the automatic, decorated features of the\n   cache - therefore you've got no fallback for instance\n2. Resilience4clj cache internally segments the cache for every\n   function that it decorates and every combination of arguments sent\n   to the function. When used manually, only one \"caching space\" is\n   used. Therefore, if the same args are used in different places with\n   different semantic meanings you will still get the same values from\n   the cache.\n3. The `put!` and `get` interfaces prefer dealing with `\u003cargs\u003e` as a\n   list. If you don send a `seqable?` as `\u003cargs\u003e`, whatever parameter\n   you send will be transformed into a list. Therefore (due to the\n   bullet above) sending `:foobar` is equivalent to `'(:foobar)`\n\nSee [using the cache as an effect](#using-as-an-effect) for a use case\nwhere direct manipulation of the cache is very useful.\n\n## Invalidating the Cache\n\nBy default Resilience4clj cache uses an eternal cache (this can be\n[set up differently if you want](#cache-settings)) therefore, you\nmight eventually want to invalidate the cache altogether.\n\nIn order to do so, use the function `invalidate!`. In the following\ncode, the `cache` will be invalidated:\n\n``` clojure\n(c/invalidate! cache)\n```\n\n## Using as an Effect\n\nResilience4clj cache is a great alternative for creating fallback\nstrategies in conjunction with other Resilience4clj libraries.\n\nSome libraries like [Resilience4clj retry][retry] and [Resilience4clj\ncircuit breaker][breaker] have a feature called _effects_ for\ncapturing side-effects. In this context, a side-effect is a handler\nfor processing the successful output of the decorated function call.\n\nFor instance, assuming that you have required\n`resilience4clj-retry.core` as `r` and `resilience4clj-retry.core` as\n`c`:\n\n``` clojure\n(def retry (r/create \"hello-retry\"))\n\n(def cache (c/create \"hello-cache\"))\n\n(defn hello [person]\n  ;; hypothetical flaky, external HTTP request\n  (str \"Hello \" person \"!!\"))\n```\n\nNow that you have a default `retry`, a default `cache`, and a\npotentially flaky function `hello` let's create an effect that puts\nthe returned value in the cache, a fallback that gets it from the\ncache and a decorated function that puts them together:\n\n``` clojure\n(defn effect-fn\n  [ret person]\n  (c/put! cache person ret))\n\n(defn fallback-fn\n  [e person]\n  (c/get cache person))\n\n(def safe-cached-hello\n  (r/decorate hello retry\n              {:effect effect-fn\n               :fallback fallback-fn}))\n```\n\nThe behavior here is that when calling the `safe-cached-hello`\nfunction, the function `hello` will be retried for a few times (max\ndefault is 3). In case of success, the returned value will be put in\nthe cache. In case of failure whatever value is on the cache will be\nreturned.\n\nOf course, this is a very naive approach as it will simply return `nil` in\na failure scenario where the cache is empty. A more advanced approach\nwould be:\n\n``` clojure\n(defn fallback-fn\n  [e person]\n  (if (c/contains? cache person)\n    (c/get cache person)\n    (throw e)))\n```\n\nIn the example above, if the the entry for `person` is still not in\nplace, the underlying expression is thrown.\n\nBy combining several modules from Resilience4clj (see [the list\nhere][about]) you can achieve very advanced behavior quickly. For\ninstance:\n\n``` clojure\n(def very-safe-hello\n  (-\u003e hello\n      (r/decorate retry {:effect effect-fn\n                         :fallback fallback-fn})\n      (tl/decorate timelimiter)\n      (cb/decorate breaker)))\n```\n\nWith the snippet above, you are retrying `hello` in case of failure,\ncaching its return when succesful, having a cached fallback strategy,\nwithin a pre-defined time limit (execution budget) and protected by a\nring-based circuit breaker.\n\nAll that in just 5 lines.\n\n## Metrics\n\nThe function `metrics` returns a map with the metrics of the cache:\n\n``` clojure\n(c/metrics cache)\n\n=\u003e {:hits 0\n    :misses 0\n    :errors 0\n    :manual-puts 0 \n    :manual-gets 0}\n```\n\nThe nodes should be self-explanatory. Because direct manipulation of\nthe cache does not go through the automatic hit/miss logic, these are\nkept separatelly.\n\nThe metrics can be reset with a call to the `reset!` function:\n\n``` clojure\n(c/reset! cache)\n```\n\nMetrics will cycle back to 0 when they reach `Long/MAX_VALUE`.\n\n## Events\n\nYou can listen to events generated by the use of the cache. This is\nparticularly useful for logging, debugging, or monitoring the health\nof your cache.\n\n``` clojure\n(def cache (c/create \"my-cache\"))\n\n(c/listen-event cache\n                 :HIT\n                 (fn [evt]\n                   (println (str \"Your cache has been hit\"))))\n```\n\nThere are six types of events:\n\n1. `:HIT` - informs that a call has hit the cache\n2. `:MISSED` - informs that a call has missed the cache\n3. `:ERROR` - informs that an error has taken place on the call\n4. `:EXPIRED` - informs that an entry has expired on the cache\n5. `:MANUAL-PUT` - informs that a manual put has happened\n6. `:MANUAL-GET` - informs that a manual get has happened\n\nNotice you have to listen to a particular type of event by specifying\nthe event-type you want to listen.\n\n**Note on `:EXPIRED`**: expiration rules differ from caching provider\nto caching provider. They might also differ depending on the way you\nhave set up your cache (see [cache settings](#cache-settings) for more\ndetails). In every practical sense, you should not rely on `:EXPIRED`\nfor anything business critical unless the behavior of your\ncache/settings is known and consistent.\n\nAll events receive a map containing the `:event-type`, the\n`:cache-name`, the event `:creation-time`, the function name that\ngenerated the entry `:fn-name`, and the internal `:key` that\nrepresents the unique id of the cache entry. For now, `:EXPIRED` does\nnot support `:fn-name`.\n\n## Exception Handling\n\nWhen using the fallback function, be aware that its signature is the\nsame as the original function plus an exception (`e` on the example\nabove). This exception is an `ExceptionInfo` wrapping around the real\ncause of the error. You can inspect the `:cause` node of this\nexception to learn about the inner exception:\n\nIf you are not using a fallback function, then you don't need to worry\nabout anything. Your exception will bubble up as you would expect.\n\n## Composing Further\n\nResilience4clj is composed of [several modules][about] that easily\ncompose together. For instance, if you are also using the [retry\nmodule][retry] and assuming your import and basic settings look like\nthis:\n\n``` clojure\n(ns my-app\n  (:require [resilience4clj-cache.core :as c]\n            [resilience4clj-retry.core :as r]))\n\n;; create a retry with default settings\n(def retry (r/create \"my-retry\"))\n\n;; create cache with default settings\n(def cache (c/create \"my-cache\"))\n\n;; flaky function you want to potentially retry\n(defn flaky-hello []\n  ;; hypothetical request to a flaky server that might fail (or not)\n  \"Hello World!\")\n```\n\nThen you can create a protected call that combines both the retry and\nthe cache:\n\n``` clojure\n(def protected-hello (-\u003e flaky-hello\n                         (r/decorate retry)\n                         (c/decorate cache)))\n```\n\nThe resulting function `protected-hello` will retry before persisting\nto cache and skip retries altogether in case of cache hits as you\nwould expect. The composing order makes a big difference of course, if\nretry and cache had been reversed here, `flaky-hello` would cache\nfirst and the retry would wrap the cache which is not what you would\nwant.\n\nThe cache module is special as it composes very nicely with a\neffect/fallback strategy. See [how to use cache as an\neffect](#using-as-an-effect).\n\n## Bugs\n\nIf you find a bug, submit a [Github issue][github-issues].\n\n## Help\n\nThis project is looking for team members who can help this project\nsucceed!  If you are interested in becoming a team member please open\nan issue.\n\n## License\n\nCopyright © 2019 Tiago Luchini\n\nDistributed under the MIT License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fresilience4clj%2Fresilience4clj-cache","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fresilience4clj%2Fresilience4clj-cache","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fresilience4clj%2Fresilience4clj-cache/lists"}