├── doc ├── mem-use.png ├── performance.png ├── manual-add-remove.md ├── ns-scan.md ├── variable-expiry.md ├── ret-fn.md ├── key-fn.md ├── tiered.md ├── cache-invalidation-concurrency.md ├── major.md ├── events.md ├── tags.md └── performance.md ├── .gitignore ├── java └── memento │ ├── mount │ ├── Cached.java │ ├── IMountPoint.java │ ├── CachedFn.java │ └── CachedMultiFn.java │ ├── multi │ ├── DaisyChainCache.java │ ├── MultiCache.java │ ├── TieredCache.java │ └── ConsultingCache.java │ ├── base │ ├── LockoutTag.java │ ├── CacheKey.java │ ├── Segment.java │ ├── EntryMeta.java │ ├── Durations.java │ ├── ICache.java │ └── LockoutMap.java │ └── caffeine │ ├── Expiry.java │ ├── SecondaryIndex.java │ ├── SpecialPromise.java │ └── CaffeineCache_.java ├── src └── memento │ ├── guava.clj │ ├── ns_scan.clj │ ├── multi.clj │ ├── base.clj │ ├── guava │ └── config.clj │ ├── caffeine │ └── config.clj │ ├── caffeine.clj │ ├── mount.clj │ ├── config.clj │ └── core.clj ├── test └── memento │ ├── ns_scan_test.clj │ ├── multi_test.clj │ ├── caffeine_test.clj │ ├── mount_test.clj │ └── core_test.clj ├── deps.edn ├── LICENSE ├── pom.xml ├── CHANGELOG.md └── README.md /doc/mem-use.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RokLenarcic/memento/HEAD/doc/mem-use.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.jar 3 | /.nrepl-port 4 | /.repl 5 | *.iml 6 | *.asc 7 | target/ 8 | -------------------------------------------------------------------------------- /doc/performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RokLenarcic/memento/HEAD/doc/performance.png -------------------------------------------------------------------------------- /java/memento/mount/Cached.java: -------------------------------------------------------------------------------- 1 | package memento.mount; 2 | 3 | import clojure.lang.IFn; 4 | 5 | public interface Cached { 6 | IMountPoint getMp(); 7 | IFn getOriginalFn(); 8 | } 9 | -------------------------------------------------------------------------------- /src/memento/guava.clj: -------------------------------------------------------------------------------- 1 | (ns memento.guava 2 | "Guava cache implementation." 3 | {:author "Rok Lenarčič"} 4 | (:require [memento.caffeine :as c])) 5 | 6 | (def stats c/stats) 7 | 8 | (def to-data c/to-data) 9 | 10 | (def load-data c/load-data) 11 | -------------------------------------------------------------------------------- /java/memento/multi/DaisyChainCache.java: -------------------------------------------------------------------------------- 1 | package memento.multi; 2 | 3 | import clojure.lang.IPersistentMap; 4 | import clojure.lang.ISeq; 5 | import memento.base.ICache; 6 | import memento.base.Segment; 7 | 8 | public class DaisyChainCache extends MultiCache { 9 | 10 | public DaisyChainCache(ICache cache, ICache upstream, IPersistentMap conf, Object absent) { 11 | super(cache, upstream, conf, absent); 12 | } 13 | 14 | @Override 15 | public Object cached(Segment segment, ISeq args) { 16 | Object c = cache.ifCached(segment, args); 17 | return c == absent ? upstream.cached(segment, args) : c; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/memento/ns_scan_test.clj: -------------------------------------------------------------------------------- 1 | (ns memento.ns-scan-test 2 | (:require [clojure.test :refer :all] 3 | [memento.core :as m] 4 | [memento.ns-scan :as ns-scan])) 5 | 6 | (defn x 7 | {::m/cache :x} 8 | [] 1) 9 | 10 | (defn y 11 | {::m/cache {::m/size< 1 ::m/type ::m/caffeine}} 12 | [] 1) 13 | 14 | (deftest test-ns-scan 15 | (testing "should attach a cache" 16 | (is (= [#'x #'y] (ns-scan/attach-caches))) 17 | (is (= true (m/memoized? x))) 18 | (is (= true (m/memoized? y)))) 19 | (testing "should attach a new cache" 20 | (let [temp x 21 | _ (ns-scan/attach-caches)] 22 | (is (not= x temp))))) 23 | -------------------------------------------------------------------------------- /doc/manual-add-remove.md: -------------------------------------------------------------------------------- 1 | # Manually evict entries 2 | 3 | You can manually evict entries: 4 | 5 | ```clojure 6 | ; invalidate everything, also works on MountPoint instances 7 | (m/memo-clear! memoized-function) 8 | ; invaliate an arg-list, also works on MountPoint instances 9 | (m/memo-clear! memoized-function arg1 arg2 ...) 10 | ``` 11 | 12 | You can manually evict all entries in a Cache instance: 13 | 14 | ```clojure 15 | (m/memo-clear-cache! cache-instance) 16 | ``` 17 | 18 | # Manually add entries 19 | 20 | You can add entries to a function's cache at any time: 21 | 22 | ```clojure 23 | ; also works on MountPoint instances 24 | (m/memo-add! memoized-function {[arg1 arg2] result}) 25 | ``` 26 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "target/classes"] 2 | :deps/prep-lib {:alias :build 3 | :fn compile-java 4 | :ensure "target/classes"} 5 | 6 | :deps {com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.8"}} 7 | 8 | :aliases {:dev {:extra-paths ["test"] 9 | :extra-deps {org.clojure/core.memoize {:mvn/version "1.1.266"} 10 | com.clojure-goes-fast/clj-memory-meter {:mvn/version "0.2.1"}} 11 | :jvm-opts ["-XX:-OmitStackTraceInFastThrow" 12 | "-Djdk.attach.allowAttachSelf"]} 13 | :build {:deps {io.github.seancorfield/build-clj {:git/tag "v0.6.4" :git/sha "c21cfde"}} 14 | :ns-default build} 15 | :test {:extra-paths ["test"] 16 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"} 17 | io.github.cognitect-labs/test-runner 18 | {:git/tag "v0.5.0" :git/sha "48c3c67"}}}}} 19 | -------------------------------------------------------------------------------- /java/memento/base/LockoutTag.java: -------------------------------------------------------------------------------- 1 | package memento.base; 2 | 3 | import java.util.Objects; 4 | import java.util.UUID; 5 | import java.util.concurrent.CountDownLatch; 6 | 7 | public class LockoutTag { 8 | private UUID id; 9 | private CountDownLatch latch; 10 | 11 | public LockoutTag(UUID id) { 12 | this.id = id; 13 | this.latch = new CountDownLatch(1); 14 | } 15 | 16 | public LockoutTag() { 17 | this(UUID.randomUUID()); 18 | } 19 | 20 | public UUID getId() { 21 | return id; 22 | } 23 | 24 | public CountDownLatch getLatch() { 25 | return latch; 26 | } 27 | 28 | @Override 29 | public boolean equals(Object o) { 30 | if (this == o) return true; 31 | if (o == null || getClass() != o.getClass()) return false; 32 | LockoutTag that = (LockoutTag) o; 33 | return Objects.equals(id, that.id); 34 | } 35 | 36 | @Override 37 | public int hashCode() { 38 | return Objects.hash(id); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2021-2023, Rok Lenarčič 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 7 | persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 10 | Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 13 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 15 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | org.clojars.roklenarcic 5 | memento 6 | jar 7 | 0.9.10 8 | memento 9 | A library for function memoization. 10 | https://github.com/RokLenarcic/memento 11 | 12 | 13 | MIT License 14 | https://github.com/RokLenarcic/memento/blob/master/LICENSE 15 | 16 | 17 | 18 | https://github.com/RokLenarcic/memento 19 | scm:git:git://github.com/RokLenarcic/memento.git 20 | scm:git:ssh://git@github.com/RokLenarcic/memento.git 21 | 22 | 23 | 24 | 25 | src 26 | 27 | 28 | -------------------------------------------------------------------------------- /java/memento/base/CacheKey.java: -------------------------------------------------------------------------------- 1 | package memento.base; 2 | 3 | import clojure.lang.Util; 4 | 5 | import java.util.Objects; 6 | 7 | public class CacheKey { 8 | private final Object id; 9 | private final Object args; 10 | 11 | private final int _hq; 12 | 13 | public CacheKey(Object id, Object args) { 14 | this.id = id; 15 | this.args = args; 16 | this._hq = 31 * id.hashCode() + Util.hasheq(args); 17 | } 18 | 19 | public Object getId() { 20 | return id; 21 | } 22 | 23 | public Object getArgs() { 24 | return args; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | CacheKey cacheKey = (CacheKey) o; 32 | return _hq == cacheKey._hq && Objects.equals(id, cacheKey.id) && Util.equiv(args, cacheKey.args); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return _hq; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return "CacheKey{" + 43 | "id=" + id + 44 | ", args=" + args + 45 | '}'; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /doc/ns-scan.md: -------------------------------------------------------------------------------- 1 | # Namespace scan 2 | 3 | You can scan loaded namespaces for annotated vars and automatically create caches. 4 | The scan looks for Vars with `:memento.core/cache` key in the meta. 5 | That value is used as a cache spec. 6 | 7 | Given require `[memento.ns-scan :as ns-scan]`: 8 | ```clojure 9 | (ns myproject.some-ns 10 | (:require 11 | [myproject.cache :as cache] 12 | [memento.core :as m])) 13 | 14 | ; defn already has a nice way for adding meta 15 | (defn test1 16 | "A function using built-in defn meta mechanism to specify a cache region" 17 | {::m/cache cache/inf} 18 | [arg1 arg2] 19 | (+ arg1 arg2)) 20 | 21 | ; you can also do standard meta syntax 22 | (defn ^{::m/cache cache/inf} test2 23 | "A function using normal meta syntax to add a cache to itself" 24 | [arg1 arg2] (+ arg1 arg2)) 25 | 26 | ; this also works on def 27 | (def ^{::m/cache cache/inf} test3 (fn [arg1 arg2] (+ arg1 arg2))) 28 | 29 | ; attach caches 30 | (ns-scan/attach-caches) 31 | ``` 32 | 33 | This only works on LOADED namespaces, so beware. 34 | 35 | Calling `attach-caches` multiple times attaches new caches, replaces existing caches. 36 | 37 | Namespaces `clojure.*` and `nrepl.*` are not scanned by default, but you can 38 | provide your own blacklists, see doc. 39 | -------------------------------------------------------------------------------- /doc/variable-expiry.md: -------------------------------------------------------------------------------- 1 | ## Variable expiry 2 | 3 | A configuration key `:memento.core/expiry` allows that you specify expiry on per 4 | tag basis. The value needs to be an instance of `memento.caffeine.Expiry` interface. 5 | 6 | If functions return nil, then the value of corresponding setting `ttl` or `fade` is used 7 | instead. 8 | 9 | Here's an example: 10 | ```clojure 11 | 12 | (def ttl-for-person 13 | (reify Expiry 14 | (ttl [this _ k v] 15 | (if (demo-user? (:id v)) 16 | ;; never changes, cache 10 days 17 | [10 :d] 18 | ;; else cache 60 seconds 19 | 60)) 20 | (fade [this _ k v]))) 21 | 22 | (m/memo #'get-user-by-id {mc/type mc/caffeine mc/expiry ttl-for-person}) 23 | ``` 24 | 25 | An implementation `memento.caffeine.config/meta-expiry` is provided. That reads meta of returned objects 26 | for keys `memento.core/ttl` and `memento.core/fade`. 27 | 28 | ```clojure 29 | 30 | (defn get-user-by-id [id] 31 | (let [ret ....] 32 | (with-meta 33 | ret 34 | {mc/ttl (if (demo-user? (:id ret)) 35 | ;; never changes, cache 10 days 36 | [10 :d] 37 | ;; else cache 60 seconds 38 | 60)}))) 39 | 40 | (m/memo #'get-user-by-id {mc/type mc/caffeine mc/expiry mc/meta-expiry}) 41 | ``` -------------------------------------------------------------------------------- /doc/ret-fn.md: -------------------------------------------------------------------------------- 1 | # Prevent caching of a specific return value 2 | 3 | If you want to prevent caching of a specific function return, you can wrap it in special record 4 | using `memento.core/do-not-cache` function. Example: 5 | 6 | ```clojure 7 | (defn get-person-by-id [db-conn account-id person-id] 8 | (if-let [person (db-get-person db-conn account-id person-id)] 9 | {:status 200 :body person} 10 | (m/do-not-cache {:status 404}))) 11 | ``` 12 | 13 | 404 responses won't get cached, and the function will be invoked every time for those ids. 14 | 15 | # Modifying returned value 16 | 17 | Sticking a piece of caching logic into your function logic isn't very clean. Instead, you can 18 | add `:memento.core/ret-fn` to cache or mount conf (or use `mc/ret-fn` value) to specify a function that can modify 19 | the return value from a cached function before it is cached. This is useful when using the `do-not-cache` function above to 20 | do the wrapping outside the function being cached. Example: 21 | 22 | ```clojure 23 | ; first argument is args, second is the returned value 24 | (defn no-cache-error-resp [[db-conn account-id person-id :as args] resp] 25 | (if (<= 400 (:status resp) 599) 26 | (m/do-not-cache resp) 27 | resp)) 28 | 29 | (defn get-person-by-id [db-conn account-id person-id] 30 | (if (nil? person-id) 31 | {:status 404} 32 | {:status 200})) 33 | 34 | (m/memo #'get-person-by-id (assoc cache/inf-cache mc/ret-fn no-cache-error-resp)) 35 | ``` 36 | 37 | **This is both a mount conf setting, and a cache setting. This has same consequences as with key-fn setting above.** 38 | -------------------------------------------------------------------------------- /doc/key-fn.md: -------------------------------------------------------------------------------- 1 | # Changing the key for cached tag 2 | 3 | Add `:memento.core/key-fn` to cache or mount config (or use `mc/key-fn` value) to specify a function with which to manipulate 4 | the key cache will use for the tag. 5 | 6 | ```clojure 7 | (defn get-person-by-id 8 | [db-conn account-id person-id] 9 | {}) 10 | 11 | ; when creating the cache key, remove db connection 12 | (m/memo #'get-person-by-id {mc/type mc/caffeine mc/key-fn #(remove db-conn? %)}) 13 | ; or use key-fn* 14 | (m/memo #'get-person-by-id {mc/type mc/caffeine 15 | mc/key-fn* (fn [db-conn account-id person-id] [account-id person-id])}) 16 | ``` 17 | 18 | When creating the cache key, remove db connection, so the cache uses `[account-id person-id]` as key. 19 | Thus calling the function with different db connection but same ids returns the cached value. 20 | 21 | Another example: 22 | ```clojure 23 | (defn return-my-user-info-json [http-request] 24 | (load-user (-> http-request :session :user-id))) 25 | 26 | ;; clearly the cache hit is based on a deeply nested property out of a huge request map 27 | ;; so we want to use that as basis for caching 28 | (m/memo #'return-my-user-info-json {mc/type mc/caffeine 29 | mc/key-fn* #(-> % :session :user-id)}) 30 | ``` 31 | 32 | **This is both a mount conf setting, and a cache setting.** The obvious difference is that specifying 33 | `key-fn` for the Cache will affect all functions using that cache and in mount conf, only that one function will 34 | be affected. If using 2-arg `memo`, then this setting is applied to mount conf. 35 | -------------------------------------------------------------------------------- /src/memento/ns_scan.clj: -------------------------------------------------------------------------------- 1 | (ns memento.ns-scan 2 | "Scan loaded namespaces for vars that have meta that 3 | specifies a cache, and attach cache to those vars." 4 | {:author "Rok Lenarčič"} 5 | (:require [memento.core :as core])) 6 | 7 | (def default-blacklist [#"^clojure\." #"^nrepl\."]) 8 | 9 | (defn not-blacklisted? 10 | "Returns true if namespace is not blacklisted." 11 | [black-list n] 12 | (let [ns-str (str (ns-name n))] 13 | (not-any? #(re-find % ns-str) black-list))) 14 | 15 | (defn memoize-if-configured 16 | "If var has :memento.core/cache meta key present, use that as memoization 17 | configuration. Returns the var or nil if not memoized." 18 | [v] 19 | (when (::core/cache (meta v)) (core/memo v) v)) 20 | 21 | (defn vars 22 | [black-list] 23 | (for [n (all-ns) 24 | :when (not-blacklisted? black-list n) 25 | v (vals (ns-interns n))] 26 | v)) 27 | 28 | (defn attach-caches 29 | "Scans loaded namespaces and attaches new caches to all vars that have 30 | :memento.core/cache key in meta of the var. Returns coll of affected vars. 31 | 32 | The value of :memento.core/cache meta key is used as conf parameter 33 | in memento.core/memo. If :memento.core/mount key is also present, then 34 | they are used as cache and conf parameters respectively. 35 | 36 | Note that ONLY the loaded namespaces are considered. 37 | 38 | You can specify a namespace black-list. It's a list of regexes, 39 | which are applied to namespace name with re-find (so you only need to match 40 | part of the name). The value defaults to default-blacklist, which 41 | blacklists clojure.* and nrepl.*" 42 | ([] 43 | (attach-caches default-blacklist)) 44 | ([ns-black-list] 45 | (filterv 46 | memoize-if-configured 47 | (vars ns-black-list)))) 48 | -------------------------------------------------------------------------------- /src/memento/multi.clj: -------------------------------------------------------------------------------- 1 | (ns memento.multi 2 | {:author "Rok Lenarčič"} 3 | (:require [memento.base :as b]) 4 | (:import (memento.base ICache) 5 | (memento.multi ConsultingCache DaisyChainCache MultiCache TieredCache))) 6 | 7 | (comment 8 | "A daisy chained cache. 9 | 10 | Entry is returned from cache IF PRESENT, otherwise upstream is hit. The returned value 11 | is NOT added to cache. 12 | 13 | After the operation the entry is either in local or upstream cache.") 14 | (defmethod b/new-cache :memento.core/daisy [conf] 15 | (let [^ICache cache (b/base-create-cache (::cache conf)) 16 | ^ICache upstream (b/base-create-cache (::upstream conf))] 17 | (DaisyChainCache. cache upstream conf b/absent))) 18 | 19 | (comment 20 | "A tiered cache. 21 | 22 | Entry is fetched from cache, delegating to upstream is not found. After the operation 23 | the entry is in both caches.") 24 | 25 | (defmethod b/new-cache :memento.core/tiered [conf] 26 | (let [^ICache cache (b/base-create-cache (::cache conf)) 27 | ^ICache upstream (b/base-create-cache (::upstream conf))] 28 | (TieredCache. cache upstream conf b/absent))) 29 | 30 | (comment 31 | "A consulting tiered cache. 32 | 33 | Entry is fetched from cache, if not found, the upstream is asked for entry if present (but not to make one 34 | in the upstream). 35 | 36 | After the operation, the entry is in local cache, upstream is unchanged.") 37 | 38 | (defmethod b/new-cache :memento.core/consulting [conf] 39 | (let [^ICache cache (b/base-create-cache (::cache conf)) 40 | ^ICache upstream (b/base-create-cache (::upstream conf))] 41 | (ConsultingCache. cache upstream conf b/absent))) 42 | 43 | (defn delegate [^MultiCache multi-cache] 44 | (.getDelegate multi-cache)) 45 | 46 | (defn upstream [^MultiCache multi-cache] 47 | (.getUpstream multi-cache)) 48 | -------------------------------------------------------------------------------- /java/memento/base/Segment.java: -------------------------------------------------------------------------------- 1 | package memento.base; 2 | 3 | import clojure.lang.IFn; 4 | import clojure.lang.IPersistentMap; 5 | 6 | import java.util.Objects; 7 | 8 | // Segment has properties: 9 | // - fn to run 10 | // - key-fn to apply for keys from this segment 11 | // - segment ID, use this rather than f to separate segments in cache 12 | // - conf is mount point (or segment) conf 13 | // - expiry is Segment specific expiry 14 | public class Segment { 15 | private final IFn f; 16 | private final IFn keyFn; 17 | private final Object id; 18 | 19 | private final IPersistentMap conf; 20 | 21 | public Segment(IFn f, IFn keyFn, Object id, IPersistentMap conf) { 22 | this.f = f; 23 | this.keyFn = keyFn; 24 | this.id = id; 25 | this.conf = conf; 26 | } 27 | 28 | public IFn getF() { 29 | return f; 30 | } 31 | 32 | public IFn getKeyFn() { 33 | return keyFn; 34 | } 35 | 36 | 37 | public Object getId() { 38 | return id; 39 | } 40 | 41 | public IPersistentMap getConf() { 42 | return conf; 43 | } 44 | 45 | @Override 46 | public boolean equals(Object o) { 47 | if (this == o) return true; 48 | if (o == null || getClass() != o.getClass()) return false; 49 | Segment segment = (Segment) o; 50 | return keyFn.equals(segment.keyFn) && id.equals(segment.id); 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | return Objects.hash(f, keyFn, id); 56 | } 57 | 58 | public Segment withFn(IFn newF) { 59 | return new Segment(newF, keyFn, id, conf); 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return "Segment{" + 65 | "f=" + f + 66 | ", keyFn=" + keyFn + 67 | ", id=" + id + 68 | ", conf=" + conf + 69 | '}'; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /java/memento/caffeine/Expiry.java: -------------------------------------------------------------------------------- 1 | package memento.caffeine; 2 | 3 | import clojure.lang.IObj; 4 | import clojure.lang.IPersistentMap; 5 | import memento.base.Durations; 6 | 7 | /** 8 | * Enables variable expiry of entries in the cache. Cache entries can have individual expiries. If interface 9 | * invocations return nil, then preexisting ttl and fade settings apply. 10 | */ 11 | public interface Expiry { 12 | /** 13 | * Return a duration of when entry expires after write, if nil is returned then ttl and fade settings are used 14 | * @param conf conf map 15 | * @param k key 16 | * @param v value 17 | * @return a duration, either an integer, representing seconds or a vector of 2 values 18 | */ 19 | Object ttl(IPersistentMap conf, Object k, Object v); 20 | /** 21 | * Return a duration of when entry expires after access, if nil is returned then ttl and fade settings are used 22 | * @param conf conf map 23 | * @param k key 24 | * @param v value 25 | * @return a duration, either an integer, representing seconds or a vector of 2 values 26 | */ 27 | Object fade(IPersistentMap conf, Object k, Object v); 28 | 29 | Expiry META_VAL_EXP = new Expiry() { 30 | 31 | @Override 32 | public Object ttl(IPersistentMap conf, Object k, Object v) { 33 | if (v instanceof IObj) { 34 | IPersistentMap meta = ((IObj) v).meta(); 35 | Object ttl = meta.valAt(Durations.ttlKw); 36 | if (ttl != null) { 37 | return ttl; 38 | } 39 | return meta.valAt(Durations.fadeKw); 40 | } 41 | return null; 42 | } 43 | 44 | @Override 45 | public Object fade(IPersistentMap conf, Object k, Object v) { 46 | if (v instanceof IObj) { 47 | IPersistentMap meta = ((IObj) v).meta(); 48 | return meta.valAt(Durations.fadeKw); 49 | } 50 | return null; 51 | } 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /java/memento/base/EntryMeta.java: -------------------------------------------------------------------------------- 1 | package memento.base; 2 | 3 | import clojure.lang.IPersistentSet; 4 | import clojure.lang.PersistentHashSet; 5 | 6 | import java.util.Objects; 7 | 8 | public class EntryMeta { 9 | 10 | public static final Object absent = new Object(); 11 | public static final EntryMeta NIL = new EntryMeta(null, false, null); 12 | 13 | public static Object unwrap(Object o) { 14 | return o instanceof EntryMeta ? ((EntryMeta) o).getV() : o; 15 | } 16 | 17 | private Object v; 18 | private boolean noCache; 19 | private IPersistentSet tagIdents; 20 | 21 | public EntryMeta(Object v, boolean noCache, IPersistentSet tagIdents) { 22 | this.v = v; 23 | this.noCache = noCache; 24 | this.tagIdents = tagIdents == null ? PersistentHashSet.EMPTY : tagIdents; 25 | } 26 | 27 | public Object getV() { 28 | return v; 29 | } 30 | 31 | public void setV(Object v) { 32 | this.v = v; 33 | } 34 | 35 | public boolean isNoCache() { 36 | return noCache; 37 | } 38 | 39 | public void setNoCache(boolean noCache) { 40 | this.noCache = noCache; 41 | } 42 | 43 | public IPersistentSet getTagIdents() { 44 | return tagIdents; 45 | } 46 | 47 | public void setTagIdents(IPersistentSet tagIdents) { 48 | this.tagIdents = tagIdents; 49 | } 50 | 51 | @Override 52 | public boolean equals(Object o) { 53 | if (this == o) return true; 54 | if (o == null || getClass() != o.getClass()) return false; 55 | EntryMeta entryMeta = (EntryMeta) o; 56 | return noCache == entryMeta.noCache && Objects.equals(v, entryMeta.v) && tagIdents.equals(entryMeta.tagIdents); 57 | } 58 | 59 | @Override 60 | public int hashCode() { 61 | return Objects.hash(v, noCache, tagIdents); 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return "EntryMeta{" + 67 | "v=" + v + 68 | ", noCache=" + noCache + 69 | ", tagIdents=" + tagIdents + 70 | '}'; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /doc/tiered.md: -------------------------------------------------------------------------------- 1 | # Tiered caching 2 | 3 | You can use caches that combine two other caches in some way. The easiest way to generate 4 | the cache configuration needed is to use `memento.core/tiered`,`memento.core/consulting`, `memento.core/daisy`. 5 | 6 | Each of these takes two parameters: 7 | - the "local" cache 8 | - the "upstream" cache 9 | 10 | These parameters can be existing `Cache` instances or cache configuration maps (in which case a new Cache will 11 | be created.) 12 | 13 | Invalidation operations on these combined caches also affect upstream. Other operations only affect local cache. 14 | 15 | ### memento.core/tiered 16 | 17 | This cache works like CPU cache would. 18 | 19 | Entry is fetched from cache, delegating to upstream is not found. After the operation 20 | the tag is in both caches. 21 | 22 | Useful when upstream is a big cache that outside the JVM, but it's not that inexpensive, so you 23 | want a local smaller cache in front of it. 24 | 25 | ### memento.core/consulting 26 | 27 | Entry is fetched from cache, if not found, the upstream is asked for tag if present (but not to make one 28 | in the upstream). 29 | 30 | After the operation, the tag is in local cache, upstream is unchanged. 31 | 32 | Useful when you want to consult a long term upstream cache for existing entries, but you don't want any 33 | entries being created for the short term cache to be pushed upstream. 34 | 35 | This is what you usually want when you put a request scoped cache in front of an existing longer cache. 36 | 37 | ```clojure 38 | (m/with-caches :request (memoize #(m/create (m/consulting inf-cache %))) 39 | ....) 40 | ``` 41 | 42 | In this kind of setup, the request scoped cache will use any entries on any caches that were 43 | on functions outside this block, but it will not introduce more entries to them. 44 | 45 | ### memento.core/daisy 46 | 47 | Entry is returned from cache IF PRESENT, otherwise upstream is hit. The returned value 48 | is NOT added to cache. 49 | 50 | After the operation the tag is either in local or upstream cache. 51 | 52 | Useful when you don't want entries from upstream accumulating in local 53 | cache, and you're feeding the local cache via some other means: 54 | - a preloaded fixed cache 55 | - manually adding entries 56 | -------------------------------------------------------------------------------- /java/memento/base/Durations.java: -------------------------------------------------------------------------------- 1 | package memento.base; 2 | 3 | import clojure.lang.ISeq; 4 | import clojure.lang.Keyword; 5 | import clojure.lang.Seqable; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | public class Durations { 12 | 13 | /** 14 | * (def timeunits 15 | * "Timeunits keywords" 16 | * {:ns TimeUnit/NANOSECONDS 17 | * :us TimeUnit/MICROSECONDS 18 | * :ms TimeUnit/MILLISECONDS 19 | * :s TimeUnit/SECONDS 20 | * :m TimeUnit/MINUTES 21 | * :h TimeUnit/HOURS 22 | * :d TimeUnit/DAYS}) 23 | */ 24 | private static final Map timeunits = init(); 25 | 26 | private static Map init() { 27 | Map m = new HashMap<>(); 28 | m.put(Keyword.intern("ns"), TimeUnit.NANOSECONDS); 29 | m.put(Keyword.intern("us"), TimeUnit.MICROSECONDS); 30 | m.put(Keyword.intern("ms"), TimeUnit.MILLISECONDS); 31 | m.put(Keyword.intern("s"), TimeUnit.SECONDS); 32 | m.put(Keyword.intern("m"), TimeUnit.MINUTES); 33 | m.put(Keyword.intern("h"), TimeUnit.HOURS); 34 | m.put(Keyword.intern("d"), TimeUnit.DAYS); 35 | return m; 36 | } 37 | 38 | public static long nanos(Object o) { 39 | if (o instanceof Number) { 40 | return TimeUnit.SECONDS.toNanos(((Number) o).longValue()); 41 | } else { 42 | ISeq s = o instanceof ISeq ? (ISeq) o : ((Seqable)o).seq(); 43 | long amt = ((Number)s.first()).longValue(); 44 | return timeunits.get(s.next().first()).toNanos(amt); 45 | } 46 | } 47 | 48 | public static long millis(Object o) { 49 | if (o instanceof Number) { 50 | return TimeUnit.SECONDS.toMillis(((Number) o).longValue()); 51 | } else { 52 | ISeq s = o instanceof ISeq ? (ISeq) o : ((Seqable)o).seq(); 53 | long amt = ((Number)s.first()).longValue(); 54 | return timeunits.get(s.next().first()).toMillis(amt); 55 | } 56 | } 57 | 58 | public static final Keyword fadeKw = Keyword.intern("memento.core", "fade"); 59 | public static final Keyword ttlKw = Keyword.intern("memento.core", "ttl"); 60 | 61 | } 62 | -------------------------------------------------------------------------------- /java/memento/mount/IMountPoint.java: -------------------------------------------------------------------------------- 1 | package memento.mount; 2 | 3 | import clojure.lang.IPersistentMap; 4 | import clojure.lang.ISeq; 5 | import memento.base.ICache; 6 | import memento.base.Segment; 7 | 8 | import java.lang.ref.Cleaner; 9 | 10 | /** 11 | * Interface for cache mount 12 | */ 13 | public interface IMountPoint { 14 | /** 15 | * Returns the cache as a map. This does not imply a snapshot, 16 | * as implementation might provide a weakly consistent view of the cache. 17 | * 18 | * @return 19 | */ 20 | IPersistentMap asMap(); 21 | 22 | /** 23 | * Return cached value, possibly invoking the function with the args to 24 | * obtain the value. This should be a thread-safe atomic operation. 25 | * 26 | * @param args 27 | * @return 28 | */ 29 | Object cached(ISeq args); 30 | 31 | /** 32 | * Return cached value if present in cache or memento.base/absent otherwise. 33 | * 34 | * @param args 35 | * @return 36 | */ 37 | Object ifCached(ISeq args); 38 | 39 | /** 40 | * Coll of tags for this mount point 41 | * 42 | * @return 43 | */ 44 | Object getTags(); 45 | 46 | /** 47 | * Handles event using internal event handling mechanism, usually a function 48 | * 49 | * @param event 50 | * @return 51 | */ 52 | Object handleEvent(Object event); 53 | 54 | /** 55 | * Invalidate entry for args, returns Cache 56 | * 57 | * @param args 58 | * @return 59 | */ 60 | ICache invalidate(ISeq args); 61 | 62 | /** 63 | * Invalidate all entries, returns Cache 64 | * 65 | * @return 66 | */ 67 | ICache invalidateAll(); 68 | 69 | /** 70 | * Returns currently mounted Cache. 71 | * 72 | * @return 73 | */ 74 | ICache mountedCache(); 75 | 76 | /** 77 | * Return segment 78 | * 79 | * @return 80 | */ 81 | Segment segment(); 82 | 83 | /** 84 | * Add entries to cache, returns Cache. 85 | * 86 | * @param argsToVals 87 | * @return 88 | */ 89 | ICache addEntries(IPersistentMap argsToVals); 90 | 91 | Cleaner cleaner = Cleaner.create(); 92 | 93 | static void register(Object guardObject, Runnable cleanupAction) { 94 | cleaner.register(guardObject, cleanupAction); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /java/memento/multi/MultiCache.java: -------------------------------------------------------------------------------- 1 | package memento.multi; 2 | 3 | import clojure.lang.IPersistentMap; 4 | import clojure.lang.ISeq; 5 | import memento.base.ICache; 6 | import memento.base.Segment; 7 | 8 | public abstract class MultiCache implements ICache { 9 | protected final ICache cache; 10 | protected final ICache upstream; 11 | private final IPersistentMap conf; 12 | protected final Object absent; 13 | 14 | public MultiCache(ICache cache, ICache upstream, IPersistentMap conf, Object absent) { 15 | this.cache = cache; 16 | this.upstream = upstream; 17 | this.conf = conf; 18 | this.absent = absent; 19 | } 20 | 21 | @Override 22 | public IPersistentMap conf() { 23 | return conf; 24 | } 25 | 26 | @Override 27 | public Object ifCached(Segment segment, ISeq args) { 28 | Object v = cache.ifCached(segment, args); 29 | return v == absent ? upstream.ifCached(segment, args) : v; 30 | } 31 | 32 | @Override 33 | public ICache invalidate(Segment segment) { 34 | cache.invalidate(segment); 35 | upstream.invalidate(segment); 36 | return this; 37 | } 38 | 39 | @Override 40 | public ICache invalidate(Segment segment, ISeq args) { 41 | cache.invalidate(segment, args); 42 | upstream.invalidate(segment, args); 43 | return this; 44 | } 45 | 46 | @Override 47 | public ICache invalidateAll() { 48 | cache.invalidateAll(); 49 | upstream.invalidateAll(); 50 | return this; 51 | } 52 | 53 | @Override 54 | public ICache invalidateIds(Iterable ids) { 55 | cache.invalidateIds(ids); 56 | upstream.invalidateIds(ids); 57 | return this; 58 | } 59 | 60 | @Override 61 | public ICache addEntries(Segment segment, IPersistentMap argsToVals) { 62 | cache.addEntries(segment, argsToVals); 63 | return this; 64 | } 65 | 66 | @Override 67 | public IPersistentMap asMap() { 68 | return cache.asMap(); 69 | } 70 | 71 | @Override 72 | public IPersistentMap asMap(Segment segment) { 73 | return cache.asMap(segment); 74 | } 75 | 76 | public ICache getDelegate() { 77 | return cache; 78 | } 79 | 80 | public ICache getUpstream() { 81 | return upstream; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /doc/cache-invalidation-concurrency.md: -------------------------------------------------------------------------------- 1 | # Invalidation and loads problems 2 | 3 | ## 1. Accessing a cached value while a bulk invalidation is in progress 4 | 5 | You might have a cached function that calls another cached function and they operate on same data, but the invalidation 6 | is not atomic so the top function is invalidated and it immediately reconstructs an tag with invalid, but still 7 | not removed, data from the other cached function. 8 | 9 | This applies to tagged invalidation. 10 | 11 | #### Solution 12 | 13 | Lockout/remove all the access to tagged entries of a specific tag until the invalidation is complete. 14 | 15 | Achieved by having a map with tag IDs that are under invalidation that is atomically updated to contain the set under 16 | invalidation, and a there are CountDownLatch objects that you can await for the invalidation to be over. 17 | 18 | #### What to do when cached value is invalidated 19 | 20 | If cached value is subject of ongoing invalidation pass, we need to recurse into a load, discarding the value. But 21 | we don't want to recurse into a load, that still starts in the middle of an invalidation pass, as that will be also discarded. 22 | 23 | In order to minimize looping of the load function calls, we want to at least try to await the current invalidation pass. 24 | 25 | ## 2. Load completes, but an invalidation has happened during that load 26 | 27 | Usually an invalidation in come in after a DB update or something like that. Any ongoing load will create some objects 28 | that will have the old data, but they won't be invalidated since the invalidation happened during load. 29 | 30 | For normal invalidation this is a simple problem, we can mark the Promise of the ongoing load as invalid, remove it from cache and retry. 31 | But for tagged invalidations, the tag IDs of the ongoing loads are not known before the result is calculated, so we need to detect 32 | all the tag id invalidations that happen after the load start. 33 | 34 | So each cache instance can have a map of ongoing loads to tag IDs that were invalidated during the load. This is combined with the 35 | lockout map to provide full coverage. 36 | 37 | It gets even harder when we consider caches outside the JVM like Redis. 38 | 39 | #### Solution 40 | 41 | On a high level we need a history of recent tag IDs invalidated and a way to tell if they pertain to the ongoing load. 42 | 43 | We need to link those facts. So a part of the invalidation sequence would be somehow tagging every ongoing load with 44 | invalidated tag IDs that happen during the load. The lockout map is a source of such a snapshot. -------------------------------------------------------------------------------- /src/memento/base.clj: -------------------------------------------------------------------------------- 1 | (ns memento.base 2 | "Memoization library with many features. 3 | 4 | memento.cache introduces Cache protocol that people need to extend when making 5 | extensions." 6 | {:author "Rok Lenarčič"} 7 | (:require [memento.config :as config]) 8 | (:import (clojure.lang AFn) 9 | (memento.base EntryMeta ICache LockoutMap))) 10 | 11 | (def absent "Value that signals absent key." EntryMeta/absent) 12 | 13 | (defn unwrap-meta [o] (if (instance? EntryMeta o) (.getV ^EntryMeta o) o)) 14 | 15 | (def ^LockoutMap lockout-map 16 | "A LockoutMap. Implementation developers use this to do caching in a fashion that is aware 17 | of bulk invalidation. " 18 | LockoutMap/INSTANCE) 19 | 20 | (def no-cache 21 | (reify ICache 22 | (conf [this] {config/type config/none}) 23 | (cached [this segment args] (unwrap-meta (AFn/applyToHelper (.getF segment) args))) 24 | (ifCached [this segment args] absent) 25 | (invalidate [this segment] this) 26 | (invalidate [this segment args] this) 27 | (invalidateAll [this] this) 28 | (invalidateIds [this ids] this) 29 | (addEntries [this f args-to-vals] this) 30 | (asMap [this] {}) 31 | (asMap [this segment] {}))) 32 | 33 | (defn conf [^ICache icache] (.conf icache)) 34 | (defn cached [^ICache icache segment args] (.cached icache segment args)) 35 | (defn if-cached [^ICache icache segment args] (.ifCached icache segment args)) 36 | (defn invalidate 37 | ([^ICache icache segment args] (.invalidate icache segment args)) 38 | ([^ICache icache segment] (.invalidate icache segment))) 39 | (defn invalidate-all [^ICache icache] (.invalidateAll icache)) 40 | (defn invalidate-ids [^ICache icache ids] (.invalidateIds icache ids)) 41 | (defn put-all [^ICache icache f args-to-vals] (.addEntries icache f args-to-vals)) 42 | (defn as-map 43 | ([^ICache icache] (.asMap icache)) 44 | ([^ICache icache segment] (.asMap icache segment))) 45 | 46 | (defmulti new-cache "Instantiate cache. Extension point, do not call directly." config/type) 47 | 48 | (defmethod new-cache :memento.core/none [_] no-cache) 49 | 50 | (defn base-create-cache 51 | "Create a cache. 52 | 53 | A conf is a map of cache settings, see memento.config namespace for names of settings." 54 | [conf] 55 | (if (instance? ICache conf) 56 | conf 57 | (cond 58 | (config/cache conf) (throw (ex-info "You are passing meta key :memento.core/cache into cache constructor. Did you mean to specify :memento.core/type?" {})) 59 | config/enabled? (new-cache (merge {config/type config/*default-type*} conf)) 60 | :else no-cache))) 61 | -------------------------------------------------------------------------------- /test/memento/multi_test.clj: -------------------------------------------------------------------------------- 1 | (ns memento.multi-test 2 | (:require [memento.core :as m] 3 | [memento.base :as b] 4 | [memento.config :as mc] 5 | [clojure.test :refer :all]) 6 | (:import (memento.base CacheKey))) 7 | 8 | (def inf-cache {mc/type mc/caffeine}) 9 | 10 | (defn as-map [cache] (reduce-kv #(assoc %1 (.getArgs ^CacheKey %2) %3) {} (b/as-map cache))) 11 | 12 | (deftest daisy-test 13 | (testing "" 14 | (let [access-count (atom 0) 15 | upstream-access-count (atom 0) 16 | c (m/create (assoc inf-cache mc/ret-fn (fn [_ r] (swap! access-count inc) r))) 17 | up (m/create (assoc inf-cache mc/ret-fn (fn [_ r] (swap! upstream-access-count inc) r))) 18 | f (m/memo inc (m/daisy c up)) 19 | _ (m/memo-add! f {[2] 3})] 20 | (is (= 1 (f 0))) 21 | (is (= 1 (f 0))) 22 | (is (= 1 (f 0))) 23 | (is (= 2 (f 1))) 24 | (is (= 3 (f 2))) 25 | (is (= {[2] 3} (m/as-map f))) 26 | (is (= {[2] 3} (as-map c))) 27 | (is (= {[0] 1 [1] 2} (as-map up))) 28 | (is (= 0 @access-count)) 29 | (is (= 2 @upstream-access-count))))) 30 | 31 | (deftest tiered-test 32 | (testing "" 33 | (let [access-count (atom 0) 34 | upstream-access-count (atom 0) 35 | c (m/create (assoc inf-cache mc/ret-fn (fn [_ r] (swap! access-count inc) r))) 36 | up (m/create (assoc inf-cache mc/ret-fn (fn [_ r] (swap! upstream-access-count inc) r))) 37 | f (m/memo inc (m/tiered c up)) 38 | _ (m/memo-add! f {[2] 3})] 39 | (is (= 1 (f 0))) 40 | (is (= 1 (f 0))) 41 | (is (= 1 (f 0))) 42 | (is (= 2 (f 1))) 43 | (is (= 3 (f 2))) 44 | (is (= {[0] 1 [1] 2 [2] 3} (m/as-map f))) 45 | (is (= {[0] 1 [1] 2 [2] 3} (as-map c))) 46 | (is (= {[0] 1 [1] 2} (as-map up))) 47 | (is (= 2 @access-count)) 48 | (is (= 2 @upstream-access-count))))) 49 | 50 | (deftest consulting-test 51 | (testing "" 52 | (let [access-count (atom 0) 53 | upstream-access-count (atom 0) 54 | c (m/create (assoc inf-cache mc/ret-fn (fn [_ r] (swap! access-count inc) r))) 55 | up (m/create (assoc inf-cache mc/ret-fn (fn [_ r] (swap! upstream-access-count inc) r))) 56 | f (m/memo inc (m/consulting c up)) 57 | _ (m/memo-add! f {[2] 3})] 58 | (is (= 1 (f 0))) 59 | (is (= 1 (f 0))) 60 | (is (= 1 (f 0))) 61 | (is (= 2 (f 1))) 62 | (is (= 3 (f 2))) 63 | (is (= {[0] 1 [1] 2 [2] 3} (m/as-map f))) 64 | (is (= {[0] 1 [1] 2 [2] 3} (as-map c))) 65 | (is (= {} (as-map up))) 66 | (is (= 2 @access-count)) 67 | (is (= 0 @upstream-access-count))))) 68 | -------------------------------------------------------------------------------- /java/memento/base/ICache.java: -------------------------------------------------------------------------------- 1 | package memento.base; 2 | 3 | import clojure.lang.IPersistentMap; 4 | import clojure.lang.ISeq; 5 | 6 | /** 7 | * Protocol for Cache. It houses entries for multiple functions. 8 | * 9 | * Most functions receive a Segment object that should be used to partition for different functions 10 | * and using other : 11 | * - id: use for separating caches, it is either name specified by user's config, or var name or function object 12 | * - key-fn: key-fn from mount point, use this to generate cache key 13 | * - f: use this function to load values 14 | */ 15 | public interface ICache { 16 | /** 17 | * Return the conf for this cache. 18 | * 19 | * @return 20 | */ 21 | IPersistentMap conf(); 22 | 23 | /** 24 | * Return the cache value. 25 | * 26 | * - segment is Segment record provided by the mount point, it contains information that allows Cache 27 | * to separate caches for different functions 28 | * 29 | * @param segment 30 | * @param args 31 | * @return 32 | */ 33 | Object cached(Segment segment, ISeq args); 34 | 35 | /** 36 | * Return cached value if present (and available immediately) in cache or memento.base/absent otherwise. 37 | * 38 | * @param segment 39 | * @param args 40 | * @return 41 | */ 42 | Object ifCached(Segment segment, ISeq args); 43 | 44 | /** 45 | * Invalidate all the entries linked a mount's single arg list, return Cache 46 | * 47 | * @param segment 48 | * @return 49 | */ 50 | ICache invalidate(Segment segment); 51 | 52 | /** 53 | * Invalidate all the entries linked to a mount, return Cache 54 | * 55 | * @param segment 56 | * @param args 57 | * @return 58 | */ 59 | ICache invalidate(Segment segment, ISeq args); 60 | 61 | /** 62 | * Invalidate all entries, returns Cache 63 | * 64 | * @return 65 | */ 66 | ICache invalidateAll(); 67 | 68 | /** 69 | * Invalidate entries with these secondary IDs, returns Cache. Each ID is a pair of tag and object 70 | * 71 | * @param id 72 | * @return 73 | */ 74 | ICache invalidateIds(Iterable id); 75 | 76 | /** 77 | * Add entries as for a function 78 | * 79 | * @param segment 80 | * @param argsToVals 81 | * @return 82 | */ 83 | ICache addEntries(Segment segment, IPersistentMap argsToVals); 84 | 85 | /** 86 | * Return all entries in the cache with keys shaped like as per cache implementation. 87 | * 88 | * @return 89 | */ 90 | IPersistentMap asMap(); 91 | 92 | /** 93 | * Return all entries in the cache for a mount with keys shaped like as per cache implementation. 94 | * 95 | * @param segment 96 | * @return 97 | */ 98 | IPersistentMap asMap(Segment segment); 99 | } 100 | -------------------------------------------------------------------------------- /doc/major.md: -------------------------------------------------------------------------------- 1 | # Caches and memoize calls 2 | 3 | ### Intro 4 | 5 | We've seen `defmemo` macro that defines a function, creates a cache based on conf, then attaches that cache. 6 | It is a combination of a `defn` to define a function and a `memento.core/memo` call to create a cache and bind it. 7 | 8 | And `memo` itself is a combination of: 9 | 10 | - creating a Cache via `memento.core/create` (optional, as you can use an existing cache) 11 | - binding the cache to the function via `memento.core/bind` (a MountPoint is used to connect a function being memoized to the cache) 12 | 13 | When `memo` or equivalent is called with a conf map, a new cache will be created and bound, if a `memento.base/Cache` instance 14 | is given that will be used instead of creating a new cache from a conf map. After `memo` or `bind` the function has a MountPoint attached. 15 | 16 | #### Creating a cache 17 | 18 | Creating a cache is done by using `memento.core/create`, which takes a map of configuration (called **cache conf**). 19 | You can use the resulting Cache with multiple functions. The configuration properties (map keys) can be found 20 | in `memento.config` and `memento.caffeine.config`, look for "Cache setting" in docstring. 21 | 22 | If `memento.config/enabled?` is false, this function always returns `memento.base/no-cache`, which is a Cache 23 | implementation that doesn't do any caching. You can set this at start-up by specifying java property: 24 | `-Dmemento.enabled=false` which globally disables caching. 25 | 26 | #### Binding the cache 27 | 28 | Binding the cache to a function is done by `memento.core/bind`. Parameters are: 29 | 30 | - a fn or a var, if var, the root value of var is changed to a memoized version 31 | - a mount point configuration or **mount conf** for short 32 | - a Cache instance that you want to bind 33 | 34 | Mount conf is either a map of mount point configuration properties, or a shorthand (see below). 35 | The configuration properties (map keys) can be found in `memento.config`, look for "function bind" in docstring. 36 | 37 | Instead of map of properties, **mount conf** can be a shorthand, which has the following two shorthands: 38 | - `[:some-keyword :another-keyword]` -> `{:memento.core/tags [:some-keyword :another-keyword]}` 39 | - `:a-keyword` -> `{:memento.core/tags [:a-keyword]}` 40 | 41 | #### Create + bind combined 42 | 43 | You can combine both functions into 1 call using `memento.core/memo`. 44 | 45 | ```clojure 46 | (m/memo fn-or-var mount-conf cache-conf) 47 | ``` 48 | 49 | To make things shorter, there's a 2-arg variant that allows that you specify both configurations at once: 50 | 51 | ```clojure 52 | (m/memo fn-or-var conf) 53 | ``` 54 | 55 | If conf is a map, then all the properties valid for mount conf are treated as such. The rest is passed to cache create. 56 | If conf is a mount conf shorthand then cache conf is considered to be {}. E.g. 57 | 58 | ```clojure 59 | (m/memo my-fn :my-tag) 60 | ``` 61 | 62 | This creates a memoized function tagged with `:my-tag` bound to a cache that does no caching. 63 | -------------------------------------------------------------------------------- /test/memento/caffeine_test.clj: -------------------------------------------------------------------------------- 1 | (ns memento.caffeine-test 2 | (:require [clojure.test :refer :all] 3 | [memento.base :as b] 4 | [memento.core :as m] 5 | [memento.config :as mc] 6 | [memento.caffeine :refer :all] 7 | [memento.caffeine.config :as mcc]) 8 | (:import (memento.base CacheKey))) 9 | 10 | #_(deftest cache-creation 11 | (testing "Creates a cache builder" 12 | (are [expected props] 13 | (= expected (str (conf->builder props nil))) 14 | "Caffeine{initialCapacity=11, removalListener}" {mc/initial-capacity 11} 15 | "Caffeine{maximumSize=29, removalListener}" {mc/size< 29} 16 | "Caffeine{maximumWeight=30, removalListener}" {mcc/weight< 30} 17 | "Caffeine{expireAfterWrite=35000000000ns, removalListener}" {mc/ttl 35} 18 | "Caffeine{expireAfterWrite=2100000000000ns, removalListener}" {mc/ttl [35 :m]} 19 | "Caffeine{expireAfterAccess=36000000000ns, removalListener}" {mc/fade 36} 20 | "Caffeine{expireAfterAccess=36000000ns, removalListener}" {mc/fade [36 :ms]} 21 | ;;"Caffeine{removalListener}" {::/refresh 37} 22 | ;;"Caffeine{removalListener}" {::mg/refresh [37 :d]} 23 | "Caffeine{removalListener}" {mcc/stats true} 24 | "Caffeine{removalListener}" {mcc/kv-weight (fn [f k v] 1)} 25 | "Caffeine{keyStrength=weak, removalListener}" {mcc/weak-keys true} 26 | "Caffeine{valueStrength=weak, removalListener}" {mcc/weak-values true} 27 | "Caffeine{valueStrength=soft, removalListener}" {mcc/soft-values true} 28 | "Caffeine{removalListener}" {mcc/ticker (fn [] (System/nanoTime))} 29 | "Caffeine{removalListener}" {mcc/removal-listener (fn [k v event] nil)} 30 | "Caffeine{initialCapacity=11, maximumWeight=30, expireAfterWrite=100000000000ns, expireAfterAccess=111000000000ns, keyStrength=weak, valueStrength=weak, removalListener}" 31 | {mc/initial-capacity 11 32 | mcc/weight< 30 33 | mc/ttl 100 34 | mc/fade 111 35 | mcc/stats true 36 | mcc/weak-keys true 37 | mcc/weak-values true 38 | mcc/removal-listener (fn [k v event] nil)})) 39 | (testing "Creates a working cache" 40 | (let [a (atom 0) 41 | builder (conf->builder {mcc/weight< 34 42 | mcc/kv-weight (fn [f k v] 20) 43 | mcc/ticker (fn [] (System/nanoTime)) 44 | mcc/removal-listener (fn [f k v event] nil)} 45 | nil) 46 | cache (.build builder)] 47 | (is (= 2 (.get cache (->CacheKey identity [1]) (java8function (fn [_] 2)))))))) 48 | 49 | (deftest data-loading-unloading 50 | (testing "Serializes" 51 | (let [c (m/memo identity {mc/type mc/caffeine mc/id "A"})] 52 | (c 1) 53 | (is (= (to-data (m/active-cache c)) {["A" '(1)] 1})))) 54 | (testing "Deserializes" 55 | (let [c (m/memo identity {mc/type mc/caffeine mc/id "A"})] 56 | (load-data (m/active-cache c) {["X" '(4)] 5}) 57 | (is (= (b/as-map (m/active-cache c)) 58 | {(CacheKey. "X" [4]) 5}))))) 59 | -------------------------------------------------------------------------------- /test/memento/mount_test.clj: -------------------------------------------------------------------------------- 1 | (ns memento.mount-test 2 | (:require [memento.mount :as m] 3 | [memento.base :as b] 4 | [memento.config :as mc] 5 | [memento.core :as core] 6 | [clojure.test :refer :all])) 7 | 8 | (deftest cache-tags-test 9 | (testing "Add tags" 10 | (is (= #:memento.mount-test{:a #{1} :b #{1} :c #{1}} 11 | (m/assoc-cache-tags {} [::a ::b ::c] 1))) 12 | (is (= #:memento.mount-test{:a #{2 3 4} :b #{2} :c #{2 3 4} :d #{2}} 13 | (-> {} 14 | (m/assoc-cache-tags [::a ::b ::c] 2) 15 | (m/assoc-cache-tags [::a ::c] 3) 16 | (m/assoc-cache-tags [::a ::c] 4) 17 | (m/assoc-cache-tags [::a ::c ::d] 2))))) 18 | (testing "Remove tags" 19 | (is (= #:memento.mount-test{:a #{3 4} :b #{} :c #{3 4} :d #{}} 20 | (-> {} 21 | (m/assoc-cache-tags [::a ::b ::c] 2) 22 | (m/assoc-cache-tags [::a ::c] 3) 23 | (m/assoc-cache-tags [::a ::c] 4) 24 | (m/assoc-cache-tags [::a ::c ::d] 2) 25 | (m/dissoc-cache-tags 2)))))) 26 | 27 | (deftest reify-mount-conf-test 28 | (is (= {:a 1 :B 3} (m/reify-mount-conf {:a 1 :B 3}))) 29 | (is (= {:memento.core/tags [1]} (m/reify-mount-conf 1))) 30 | (is (= {:memento.core/tags [1 2 3]} (m/reify-mount-conf [1 2 3])))) 31 | 32 | (deftest update-existing-test 33 | (is (= {:a 1 :b 2} (m/update-existing {:a 1 :b 1} [:b :c] inc)))) 34 | 35 | (defn test-fn [x] nil) 36 | (def test-fn-saved test-fn) 37 | 38 | (deftest id-test 39 | (let [x inc] 40 | (are [expected-id fn-or-var conf] 41 | (= expected-id (-> (m/bind fn-or-var conf b/no-cache) .getMp .segment .getId)) 42 | test-fn-saved test-fn {} 43 | test-fn-saved test-fn :a 44 | "#'memento.mount-test/test-fn" #'test-fn {} 45 | :x #'test-fn {mc/id :x} 46 | :x test-fn {mc/id :x}))) 47 | 48 | (def a (atom 0)) 49 | 50 | (defn add-prefix 51 | [x] 52 | (swap! a inc) 53 | (str "prefix-" x)) 54 | 55 | (defn add-suffix 56 | [x] 57 | (swap! a + 10) 58 | (str (add-prefix x) "-suffix")) 59 | 60 | (core/memo #'add-prefix {mc/tags [:test]}) 61 | (core/memo #'add-suffix {mc/tags [:test]}) 62 | 63 | (defn fib 64 | [x] 65 | (if (<= x 1) 1 (+ (fib (dec x)) (fib (dec (dec x)))))) 66 | 67 | (core/memo #'fib {mc/tags [:test]}) 68 | 69 | (deftest recursive-call-test 70 | (testing "Call same cache recursively." 71 | (let [_ (reset! a 0) 72 | c (core/create {mc/type mc/caffeine})] 73 | (is (= "prefix-A-suffix" 74 | (core/with-caches 75 | :test (constantly c) 76 | (add-suffix "A") 77 | (add-suffix "A") 78 | (add-suffix "A")))) 79 | (is (= @a 11)))) 80 | (testing "Call same cache function recursively." 81 | (let [c (core/create {mc/type mc/caffeine 82 | mc/initial-capacity 4})] 83 | (core/with-caches 84 | :test (constantly c) 85 | (is (= 10946 (fib 20))))))) 86 | 87 | (defmulti odd-even (fn [x] (odd? x))) 88 | (defonce orig-multi-fn odd-even) 89 | 90 | (core/memo #'odd-even {mc/type mc/caffeine}) 91 | 92 | (deftest multi-method-test 93 | (let [access-count (atom 0)] 94 | (testing "Allows multimethod utilites" 95 | (defmethod odd-even true [x] 96 | (swap! access-count inc) 97 | (println x " is odd")) 98 | (remove-all-methods odd-even) 99 | (defmethod odd-even false [x] 100 | (swap! access-count inc) 101 | (str x " is even")) 102 | (is (thrown? Exception (odd-even 1))) 103 | (is (= "2 is even" (odd-even 2)) ) 104 | (is (some? (get-method odd-even false))) 105 | (is (nil? (get-method odd-even true)))) 106 | (testing "Is cached" 107 | (odd-even 2) 108 | (odd-even 2) 109 | (odd-even 2) 110 | (is (= 1 @access-count)) 111 | (is (= orig-multi-fn (core/memo-unwrap odd-even))) 112 | (is (= true (core/memoized? odd-even)))))) -------------------------------------------------------------------------------- /src/memento/guava/config.clj: -------------------------------------------------------------------------------- 1 | (ns memento.guava.config 2 | "Guava implementation config helpers. 3 | 4 | Contains documented definitions of standard options of Guava cache config." 5 | {:author "Rok Lenarčič"}) 6 | 7 | (def ^:deprecated removal-listener 8 | "Cache setting, corresponds to .removalListener on CacheBuilder. 9 | 10 | A function of four arguments (fn [f key value removal-cause] nil), 11 | that will be called whenever an entry is removed. 12 | 13 | The four arguments are: 14 | 15 | - the function being cached 16 | - the key (arg-list transformed by key-fn if any) 17 | - the value (after ret-fn being applied) 18 | - com.google.common.cache.RemovalCause 19 | 20 | Warning: any exception thrown by listener will not be propagated to the Cache user, only logged via a Logger." 21 | (identity :memento.caffeine/removal-listener)) 22 | 23 | (def ^:deprecated weight< 24 | "Cache setting, a long. 25 | 26 | Specifies the maximum weight of entries the cache may contain. If using this option, 27 | you must provide `kw-weight` option for cache to calculate the weight of entries. 28 | 29 | A cache may evict entries before the specified limit is reached." 30 | (identity :memento.caffeine/weight<)) 31 | 32 | (def ^:deprecated kv-weight 33 | "Cache setting, a function of 3 arguments (fn [f key value] int-weight), 34 | that will be used to determine the weight of entries. 35 | 36 | It should return an int, the weight of the entry. 37 | 38 | The 3 arguments are: 39 | - the first argument is the function being cached 40 | - the second argument is the key (arg-list transformed by key-fn if any) 41 | - the third argument is the value (after ret-fn being applied)" 42 | (identity :memento.caffeine/kv-weight)) 43 | 44 | (def ^:deprecated weak-keys 45 | "Cache setting, corresponds to .weakKeys on CacheBuilder. 46 | 47 | Boolean flag, enabling storing keys using weak references. 48 | 49 | Specifies that each key (not value) stored in the cache should be wrapped in a WeakReference 50 | (by default, strong references are used). 51 | 52 | Warning: when this method is used, the resulting cache will use identity (==) comparison 53 | to determine equality of keys. Its Cache.asMap() view will therefore technically violate 54 | the Map specification (in the same way that IdentityHashMap does). 55 | 56 | The identity comparison makes this not very useful." 57 | (identity :memento.caffeine/weak-keys)) 58 | 59 | (def ^:deprecated weak-values 60 | "Cache setting, corresponds to .weakValues on CacheBuilder. 61 | 62 | Boolean flag, enabling storing values using weak references. 63 | 64 | This allows entries to be garbage-collected if there are no other (strong or soft) references to the values." 65 | (identity :memento.caffeine/weak-values)) 66 | 67 | (def ^:deprecated soft-values 68 | "Cache setting, corresponds to .softValues on CacheBuilder. 69 | 70 | Boolean flag, enabling storing values using soft references. 71 | 72 | Softly referenced objects are garbage-collected in a globally least-recently-used manner, 73 | in response to memory demand. Because of the performance implications of using soft references, 74 | we generally recommend using the more predictable maximum cache size instead." 75 | (identity :memento.caffeine/soft-values)) 76 | 77 | (def ^:deprecated stats 78 | "Cache setting, boolean flag, enabling collection of stats. 79 | 80 | Corresponds to .enableStats on CacheBuilder. 81 | 82 | You can retrieve a cache's stats by using memento.caffeine/stats. 83 | 84 | Returns com.google.common.cache.CacheStats instance or nil." 85 | (identity :memento.caffeine/stats)) 86 | 87 | (def ^:deprecated ticker 88 | "Cache setting, corresponds to .ticker on CacheBuilder. 89 | 90 | A function of zero arguments that should return current nano time. 91 | This is used when doing time based eviction. 92 | 93 | The default is (fn [] (System/nanoTime)). 94 | 95 | This is useful for testing and you can also make the time move in discrete amounts (e.g. you can 96 | make all cache accesses in a request have same time w.r.t. eviction)." 97 | (identity :memento.caffeine/ticker)) 98 | -------------------------------------------------------------------------------- /java/memento/caffeine/SecondaryIndex.java: -------------------------------------------------------------------------------- 1 | package memento.caffeine; 2 | 3 | import clojure.lang.ISeq; 4 | import memento.base.CacheKey; 5 | import memento.base.EntryMeta; 6 | 7 | import java.lang.ref.ReferenceQueue; 8 | import java.lang.ref.WeakReference; 9 | import java.util.HashSet; 10 | import java.util.Objects; 11 | import java.util.Set; 12 | import java.util.concurrent.ConcurrentHashMap; 13 | import java.util.function.Consumer; 14 | 15 | public class SecondaryIndex { 16 | 17 | private final ConcurrentHashMap> lookup; 18 | 19 | public SecondaryIndex(int concurrency) { 20 | this.lookup = new ConcurrentHashMap<>(16, 0.75f, concurrency); 21 | } 22 | 23 | /** 24 | * Add entry to secondary index. 25 | * k is CacheKey of incoming Cache entry 26 | * v is value of incoming cache entry, might be EntryMeta, if it is then we use each tag-idents 27 | * as key (id) pointing to a HashSet of CacheKeys. 28 | * 29 | * For each ID we add CacheKey to its HashSet. 30 | * 31 | * @param k 32 | * @param v 33 | */ 34 | public void add(CacheKey k, Object v) { 35 | if (v instanceof EntryMeta) { 36 | EntryMeta e = ((EntryMeta) v); 37 | ISeq s = e.getTagIdents().seq(); 38 | while (s != null) { 39 | Set cacheKeys = lookup.computeIfAbsent(s.first(), key -> new HashSet<>()); 40 | synchronized (cacheKeys) { 41 | cacheKeys.add(new IndexEntry(cacheKeys, k)); 42 | } 43 | s = s.next(); 44 | } 45 | } 46 | } 47 | 48 | public void drainKeys(Object tagId, Consumer onValue) { 49 | Set entries = lookup.remove(tagId); 50 | if (entries != null) { 51 | synchronized (entries) { 52 | for (IndexEntry e : entries) { 53 | CacheKey c = e.get(); 54 | if (c != null) { 55 | onValue.accept(c); 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | 63 | private static final ReferenceQueue evicted = new ReferenceQueue<>(); 64 | 65 | public static class IndexEntry extends WeakReference { 66 | private final Set home; 67 | private final int hash; 68 | 69 | public IndexEntry(Set home, CacheKey key) { 70 | super(key, evicted); 71 | this.hash = key.hashCode(); 72 | this.home = home; 73 | } 74 | 75 | public void delete() { 76 | synchronized (home) { 77 | home.remove(this); 78 | } 79 | } 80 | 81 | @Override 82 | // this is only used when adding entries, so we can expect this to have the underlying key here 83 | public boolean equals(Object o) { 84 | if (this == o) return true; 85 | if (o instanceof IndexEntry) { 86 | IndexEntry that = (IndexEntry) o; 87 | return hash == that.hash && Objects.equals(get(), that.get()); 88 | } else { 89 | return false; 90 | } 91 | } 92 | 93 | @Override 94 | // this is special hashcode, it remembers the key object's hash so we can kinda use hashset 95 | public int hashCode() { 96 | return hash; 97 | } 98 | } 99 | 100 | public static class Cleaner implements Runnable { 101 | 102 | @Override 103 | public void run() { 104 | while (true) { 105 | try { 106 | IndexEntry ref = (IndexEntry) evicted.remove(); 107 | ref.delete(); 108 | } catch (InterruptedException e) { 109 | // 110 | } 111 | } 112 | } 113 | } 114 | 115 | public static final Thread cleanerThread = new Thread(new Cleaner(), "Memento Secondary Index Cleaner"); 116 | 117 | static { 118 | cleanerThread.setDaemon(true); 119 | cleanerThread.start(); 120 | } 121 | 122 | 123 | } 124 | -------------------------------------------------------------------------------- /doc/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | You can fire an event at a memoized function. The target can be a particular function (or MountPoint), or 3 | you can specify a tag (and all tagged functions get the event). Each function can configure its own handler for 4 | events. Event can be any object, I suggest you use a structure that will enable event handlers to distinguish 5 | events. 6 | 7 | Event handler is a function of two arguments, the MountPoint it's been triggered (most core functions work on those) 8 | and the event. 9 | 10 | Main use case is to enable adding entries to different functions from same data. Example: 11 | 12 | ```clojure 13 | (defn get-project-name 14 | "Returns project name" 15 | [project-id]) 16 | 17 | (m/memo #'get-project-name inf) 18 | 19 | (defn get-project-owner 20 | "Returns project's owner user ID" 21 | [project-id]) 22 | 23 | (m/memo #'get-project-owner inf) 24 | 25 | (defn get-user-projects 26 | "Returns a big expensive list" 27 | [user-id] 28 | (let [project-list '...] 29 | project-list)) 30 | ``` 31 | 32 | In that example, when `get-user-projects` is called, we might load over a 100 projects, and we'd hate to waste that 33 | and not inform `get-project-name` and `get-project-owner` about the facts we've established here, especially since we 34 | might be calling these smaller functions in a loop right after fetching the big list. 35 | 36 | Here's a way to make sure data is reused by manually pushing entries into the caches as supported by most caching libs: 37 | 38 | ```clojure 39 | (defn get-user-projects 40 | "Returns a big expensive list" 41 | [user-id] 42 | (let [project-list '...] 43 | ;; preload entries for seen projects into caches 44 | (m/memo-add! get-project-name 45 | (zipmap (map (comp list :id) project-list) 46 | (map :name project-list))) 47 | (m/memo-add! get-project-owner 48 | (zipmap (map (comp list :id) project-list) 49 | (repeat user-id))) 50 | project-list)) 51 | ``` 52 | 53 | The problem with this solution is that it is an absolute nightmare to maintain: 54 | - adding/removing data consuming functions like `get-project-name` means that I have to also fix producing 55 | functions like `get-user-projects` 56 | - worse yet, the producer function has to be aware of what the argument list of consuming function looks like 57 | and how the output of that function is related to that. For instance if I change arg list for `get-project-owner` 58 | I must fix the `get-user-projects` code that pushes cache entries 59 | - if I want additional producers like `get-user-projects` then each such producer must implement all these changes 60 | and each has a massive block to feed all the consumers 61 | 62 | 63 | I can use events instead and co-locate the code that feeds the cache with the function: 64 | 65 | ```clojure 66 | (defn get-project-name 67 | "Returns project name" 68 | [project-id]) 69 | 70 | (m/memo #'get-project-name 71 | (assoc inf 72 | mc/evt-fn (m/evt-cache-add 73 | :project-seen 74 | (fn [{:keys [name id]}] {[id] name})) 75 | mc/tags [:project])) 76 | 77 | (defn get-project-owner 78 | "Returns project's owner user ID" 79 | [project-id]) 80 | 81 | (m/memo #'get-project-owner 82 | (assoc inf 83 | mc/evt-fn (m/evt-cache-add 84 | :project-seen 85 | (fn [{:keys [id user-id]}] {[id] user-id})) 86 | mc/tags [:project])) 87 | 88 | (defn get-user-projects 89 | "Returns a big expensive list" 90 | [user-id] 91 | (let [project-list '...] 92 | (doseq [p project-list] 93 | (m/fire-event! :project [:project-seen (assoc p :user-id user-id)])) 94 | project-list)) 95 | ``` 96 | 97 | We're using the `evt-cache-add` convenience function that assumes event shape is a 98 | vector of type + payload and that the intent is to add entries to the cache. 99 | 100 | In this case the producer function is only concerned with firing events at tagged caches. 101 | It doesn't need to consider the number of shape of consumers. 102 | 103 | The caching declaration of consumer functions is where there the cache feeding logic is located, 104 | which makes things manageable. 105 | -------------------------------------------------------------------------------- /doc/tags.md: -------------------------------------------------------------------------------- 1 | # Tags 2 | 3 | You can add tags to the caches. You can run actions on caches with specific tags. 4 | 5 | You can specify them via `:memento.core/tags` key (also `mc/tags` value), 6 | or you can simply specify them instead of conf map, which creates a tagged cache 7 | of noop type (that you can replace later). 8 | 9 | ```clojure 10 | (m/memo {mc/tags [:request-scope :person]} #'get-person-by-id) 11 | (m/memo [:request-scope :person] #'get-person-by-id) 12 | (m/memo :person #'get-person-by-id) 13 | ``` 14 | 15 | #### Utility 16 | 17 | You can fetch tags on a memoized function. 18 | 19 | ```clojure 20 | (m/tags get-person-by-id) 21 | => [:person] 22 | ``` 23 | 24 | You can fetch all mount points of functions that are tagged by a specific tag: 25 | 26 | ```clojure 27 | (m/mounts-by-tag :person) 28 | => #{#memento.mount.TaggedMountPoint{...}} 29 | ``` 30 | 31 | #### Change / update cache within a scope 32 | 33 | ```clojure 34 | (m/with-caches :person (constantly (m/create cache/inf-cache)) 35 | (get-person-by-id db-spec 1 12) 36 | (get-person-by-id db-spec 1 12) 37 | (get-person-by-id db-spec 1 12)) 38 | ``` 39 | 40 | Every memoized function (mountpoint) inside the block has its cache updated to the result of the 41 | provided function. In this example, all the `:person` tagged functions will use the same unbounded cache 42 | within the block. This effectively stops them from using any previously cached values and any values added to 43 | cache are dropped when block is exited. 44 | 45 | **This is extremely useful to achieve request scoped caching.** 46 | 47 | #### Updating / changing cache instance permanently 48 | 49 | You can update Cache instances of all functions tagged by a specific tag. This will modify root binding 50 | if not inside `with-caches`, otherwise it will modify the binding. 51 | 52 | ```clojure 53 | (m/update-tag-caches! :person (constantly (m/create cache/inf-cache))) 54 | ``` 55 | 56 | All `:person` tagged memoized functions will from this point on use a new empty unbounded cache. 57 | 58 | #### Applying operations to tagged memoized functions 59 | 60 | Use `mounts-by-tag` to grab mount points and then apply any of the core functions to them. 61 | 62 | ```clojure 63 | (doseq [f (m/mounts-by-tag :person)] 64 | (m/memo-clear! f)) 65 | ``` 66 | 67 | #### Invalidate entries by a tag + ID combo 68 | 69 | You can add tag + ID pairs to cached values. This can be later used to invalidate these 70 | entried based on that ID. 71 | 72 | ID can be a number like `1` or something complex like a `[1 {:region :us}]`. You can attach multiple 73 | IDs for same tag. 74 | 75 | You can add the tag ID pair inside the cached function or in the ret-fn: 76 | 77 | ```clojure 78 | (defn get-person-by-id [db-conn account-id person-id] 79 | (if (nil? person-id) 80 | {:status 404} 81 | (-> {:status 200} 82 | (m/with-tag-id :person person-id) 83 | (m/with-tag-id :account account-id)))) 84 | 85 | (m/memo #'get-person-by-id [:person :account] cache/inf-cache) 86 | ``` 87 | 88 | Now you can invalidate all entries linked to a specified ID in any correctly tagged cache: 89 | 90 | ```clojure 91 | (m/memo-clear-tag! :account 1) 92 | ``` 93 | 94 | This will invalidate entries with tag id `:account, 1` in all `:account` tagged functions. 95 | 96 | As mentioned, you can move code that adds the id information to a `ret-fn`: 97 | 98 | ```clojure 99 | ; first argument is args, second is the returned value 100 | (defn ret-fn [[_ account-id person-id :as args] resp] 101 | (if (<= 400 (:status resp) 599) 102 | (m/do-not-cache resp) 103 | (-> resp 104 | ; we can grab the data from arg list 105 | (m/with-tag-id :account account-id) 106 | (m/with-tag-id :person person-id) 107 | ; or we can grab it from the return value 108 | (m/with-tag-id :person (:id resp))))) 109 | 110 | (defn get-person-by-id [db-conn account-id person-id] 111 | (if (nil? person-id) 112 | {:status 404} 113 | {:status 200 :id person-id :name ....})) 114 | 115 | (m/memo #'get-person-by-id [:person :account] (assoc cache/inf-cache mc/ret-fn ret-fn)) 116 | ``` 117 | 118 | Later you can invalidate tagged entries: 119 | 120 | ```clojure 121 | (m/memo-clear-tag! :person 1) 122 | 123 | ;; get better atomicity with bulk operation 124 | 125 | (m/memo-clear-tags! [:person 1] [:user 33]) 126 | ``` 127 | 128 | ## Invalidation atomicity 129 | 130 | ``` 131 | 132 | ``` 133 | -------------------------------------------------------------------------------- /java/memento/base/LockoutMap.java: -------------------------------------------------------------------------------- 1 | package memento.base; 2 | 3 | import clojure.lang.IPersistentSet; 4 | import clojure.lang.ISeq; 5 | import clojure.lang.ITransientMap; 6 | import clojure.lang.PersistentHashMap; 7 | 8 | import java.util.concurrent.CopyOnWriteArraySet; 9 | import java.util.concurrent.atomic.AtomicReference; 10 | 11 | /** 12 | * This class represents a global map of ongoing bulk invalidations of Tag Ids. Await lockout can be used 13 | * to await for bulk invalidation to finish. Adding listeners is used to enable implementations to 14 | * be able to communicate these lockouts outside the JVM. 15 | */ 16 | public class LockoutMap { 17 | 18 | public static LockoutMap INSTANCE = new LockoutMap(); 19 | 20 | private final AtomicReference m = new AtomicReference<>(PersistentHashMap.EMPTY); 21 | 22 | public LockoutMap() { 23 | 24 | } 25 | 26 | private final CopyOnWriteArraySet listeners = new CopyOnWriteArraySet<>(); 27 | 28 | public void addListener(Listener l) { 29 | listeners.add(l); 30 | } 31 | 32 | /** 33 | * Add a new lockout, returning the Latch, (if newer than existing) for the given keys 34 | * 35 | * @param tagsAndIds 36 | * @return 37 | */ 38 | public void startLockout(Iterable tagsAndIds, LockoutTag tag) { 39 | PersistentHashMap oldMap; 40 | PersistentHashMap newv; 41 | do { 42 | oldMap = m.get(); 43 | ITransientMap newMap = oldMap.asTransient(); 44 | for (Object e : tagsAndIds) { 45 | newMap.assoc(e, tag); 46 | } 47 | newv = (PersistentHashMap) newMap.persistent(); 48 | } while (!m.compareAndSet(oldMap, newv)); 49 | listeners.forEach(l -> l.startLockout(tagsAndIds, tag)); 50 | } 51 | 52 | /** 53 | * End lockout for keys and the marker. After map is updated, marker's latch is released 54 | * 55 | * @param tagsAndIds 56 | * @param tag 57 | */ 58 | public void endLockout(Iterable tagsAndIds, LockoutTag tag) { 59 | PersistentHashMap oldMap; 60 | PersistentHashMap newv; 61 | do { 62 | oldMap = m.get(); 63 | ITransientMap newMap = oldMap.asTransient(); 64 | for (Object e : tagsAndIds) { 65 | if (oldMap.get(e) == tag) { 66 | newMap.without(e); 67 | } 68 | } 69 | newv = (PersistentHashMap) newMap.persistent(); 70 | } while (!m.compareAndSet(oldMap, newv)); 71 | try { 72 | listeners.forEach(l -> l.endLockout(tagsAndIds, tag)); 73 | } finally { 74 | tag.getLatch().countDown(); 75 | } 76 | } 77 | 78 | private static boolean awaitMarker(PersistentHashMap lockouts, Object obj) throws InterruptedException { 79 | LockoutTag lockoutTag = (LockoutTag) lockouts.get(obj); 80 | if (lockoutTag != null) { 81 | lockoutTag.getLatch().await(); 82 | return true; 83 | } else { 84 | return false; 85 | } 86 | } 87 | 88 | /** 89 | * It awaits an invalidations to finish, returns after that. Returns true if the entry 90 | * was invalid and an invalidation was awaited. 91 | */ 92 | public static boolean awaitLockout(Object promiseValue) throws InterruptedException { 93 | if (promiseValue instanceof EntryMeta) { 94 | IPersistentSet idents = ((EntryMeta) promiseValue).getTagIdents(); 95 | if (idents.count() != 0) { 96 | PersistentHashMap invalidations = LockoutMap.INSTANCE.m.get(); 97 | if (invalidations.isEmpty()) { 98 | return false; 99 | } 100 | ISeq identSeq = ((EntryMeta) promiseValue).getTagIdents().seq(); 101 | boolean ret = false; 102 | while (identSeq != null) { 103 | ret |= awaitMarker(invalidations, identSeq.first()); 104 | identSeq = identSeq.next(); 105 | } 106 | return ret; 107 | } 108 | } 109 | return false; 110 | } 111 | 112 | public interface Listener { 113 | void startLockout(Iterable tagsAndIds, LockoutTag latch); 114 | 115 | void endLockout(Iterable tagsAndIds, LockoutTag latch); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/memento/caffeine/config.clj: -------------------------------------------------------------------------------- 1 | (ns memento.caffeine.config 2 | "Caffeine implementation config helpers. 3 | 4 | Contains documented definitions of standard options of Caffeine cache config." 5 | {:author "Rok Lenarčič"} 6 | (:import (memento.caffeine Expiry))) 7 | 8 | (def removal-listener 9 | "Cache setting, corresponds to .removalListener on Caffeine. 10 | 11 | A function of four arguments (fn [f key value removal-cause] nil), 12 | that will be called whenever an entry is removed. 13 | 14 | The four arguments are: 15 | 16 | - the function being cached 17 | - the key (arg-list transformed by key-fn if any) 18 | - the value (after ret-fn being applied) 19 | - com.github.benmanes.caffeine.cache.RemovalCause 20 | 21 | Warning: any exception thrown by listener will not be propagated to the Cache user, only logged via a Logger." 22 | :memento.caffeine/removal-listener) 23 | 24 | (def weight< 25 | "Cache setting, a long. 26 | 27 | Specifies the maximum weight of entries the cache may contain. If using this option, 28 | you must provide `kw-weight` option for cache to calculate the weight of entries. 29 | 30 | A cache may evict entries before the specified limit is reached." 31 | :memento.caffeine/weight<) 32 | 33 | (def kv-weight 34 | "Cache setting, a function of 3 arguments (fn [f key value] int-weight), 35 | that will be used to determine the weight of entries. 36 | 37 | It should return an int, the weight of the entry. 38 | 39 | The 3 arguments are: 40 | - the first argument is the function being cached 41 | - the second argument is the key (arg-list transformed by key-fn if any) 42 | - the third argument is the value (after ret-fn being applied)" 43 | :memento.caffeine/kv-weight) 44 | 45 | ;; makes no sense, since user cannot hold on to our CacheKey instances 46 | #_(def weak-keys 47 | "Cache setting, corresponds to .weakKeys on CacheBuilder. 48 | 49 | Boolean flag, enabling storing keys using weak references. 50 | 51 | Specifies that each key (not value) stored in the cache should be wrapped in a WeakReference 52 | (by default, strong references are used). 53 | 54 | Warning: when this method is used, the resulting cache will use identity (==) comparison 55 | to determine equality of keys. Its Cache.asMap() view will therefore technically violate 56 | the Map specification (in the same way that IdentityHashMap does). 57 | 58 | The identity comparison makes this not very useful." 59 | :memento.caffeine/weak-keys) 60 | 61 | (def weak-values 62 | "Cache setting, corresponds to .weakValues on CacheBuilder. 63 | 64 | Boolean flag, enabling storing values using weak references. 65 | 66 | This allows entries to be garbage-collected if there are no other (strong or soft) references to the values." 67 | :memento.caffeine/weak-values) 68 | 69 | (def soft-values 70 | "Cache setting, corresponds to .softValues on CacheBuilder. 71 | 72 | Boolean flag, enabling storing values using soft references. 73 | 74 | Softly referenced objects are garbage-collected in a globally least-recently-used manner, 75 | in response to memory demand. Because of the performance implications of using soft references, 76 | we generally recommend using the more predictable maximum cache size instead." 77 | :memento.caffeine/soft-values) 78 | 79 | (def stats 80 | "Cache setting, boolean flag, enabling collection of stats. 81 | 82 | Corresponds to .enableStats on CacheBuilder. 83 | 84 | You can retrieve a cache's stats by using memento.caffeine/stats. 85 | 86 | Returns com.google.common.cache.CacheStats instance or nil." 87 | :memento.caffeine/stats) 88 | 89 | (def ticker 90 | "Cache setting, corresponds to .ticker on CacheBuilder. 91 | 92 | A function of zero arguments that should return current nano time. 93 | This is used when doing time based eviction. 94 | 95 | The default is (fn [] (System/nanoTime)). 96 | 97 | This is useful for testing and you can also make the time move in discrete amounts (e.g. you can 98 | make all cache accesses in a request have same time w.r.t. eviction)." 99 | :memento.caffeine/ticker) 100 | 101 | (def expiry 102 | "A cache and function bind setting, an instance of memento.caffeine.Expiry interface. 103 | 104 | Enables user to specify cached entry expiry on each entry individually. If interface 105 | functions return nil, then ttl and fade settings apply." 106 | :memento.caffeine/expiry) 107 | 108 | (def meta-expiry 109 | "A memento.caffeine.Expiry instance that looks for fade and ttl keys on object metas and uses those to control 110 | variable expiry." 111 | Expiry/META_VAL_EXP) -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.68 4 | 5 | - correctly wraps MultiFn 6 | 7 | ## 2.0.65 8 | 9 | - added a check that will throw an error if encountering mc/cache key in wrong place, cache configuration 10 | 11 | ## 2.0.63 12 | 13 | - upgrade from Caffeine 2 to Caffeine 3. Min Java changed from 8 to 11. 14 | 15 | ## 1.4.62 16 | 17 | - remove unneeded operation when adding entries into the map 18 | 19 | ## 1.4.61 20 | 21 | - when an tag is invalidated during load, the loading thread will be interrupted 22 | 23 | ## 1.3.60 24 | 25 | - improved variable expiry 26 | - added stronger prevention of the use of invalid entries 27 | 28 | ## 1.2.59 29 | 30 | - added variable expiry option (see README) 31 | - removed some reflection 32 | - enabled weakValues as an option for caffeine cache 33 | - removed weakKeys as it's not possible to use that option 34 | 35 | ## 1.2.58 36 | 37 | - added ret-ex-fn option to transform exceptions being thrown by cache in the same way ret-fn works for values 38 | 39 | ## 1.2.57 40 | 41 | - add predicate memento.core/none-cache? that checks if cache is of none type 42 | - new function memento.core/caches-by-tag 43 | - important improvement of atomicity for invalidations by tag or function 44 | - important fix for thread synchronization when adding tagged entries 45 | - important fix for secondary indexes clearing 46 | - reduced memory use 47 | - improving performance on evictions when an eviction listener isn't used 48 | - *BREAKING CHANGE FOR IMPLEMENTATIONS* `invalidateId` is now `invalidateIds` and takes an iterable of tag ids, the implementations are expected to take care to block loads until invalidations are complete. Use the `memento.base/lockout-map` for this purpose. 49 | 50 | ## 1.1.54 51 | 52 | - Improve handling of Vectors when adding entries 53 | 54 | ## 1.1.53 55 | 56 | - improve memory use 57 | 58 | ## 1.1.52 59 | 60 | - fixes a bug with concurrent loads causing some of them to return nil as a result 61 | 62 | ## 1.1.51 63 | 64 | - DO NOT USE 65 | - add check for cyclical loads to caffeine cache, e.g. cached function calling itself with same parameters, this now throws StackOverflowError, which is the error you'd get in this situation with uncached function 66 | - improved performance 67 | 68 | ## 1.1.50 69 | 70 | - added option of using SoftReferences in caffeine cache 71 | - fixed reload-guards? var not being redefinable 72 | 73 | ## 1.1.45 74 | 75 | - add getters/setters to MultiCache for the delegate/upstream cache, also add clojure functions to access these properties to `memento.multi` namespace 76 | - moved CacheKey to memento.base Java package 77 | 78 | ## 1.1.44 79 | 80 | - fix bug which would have the cache return nil when concurrently accessing a value being calculated that ends being uncacheable 81 | 82 | ## 1.1.42 83 | - big internal changes now uses Java objects for most things for smaller memory profile and smaller callstack 84 | - significant improvements to callstack size for cached call 85 | - **This is breaking changes for any implementation, shouldn't affect users** 86 | - Fixes issue where namespace scan would stack caches on the same function over and over if called multiple times 87 | 88 | Here is an example of previous callstack of recursively cached call (without ret-fn): 89 | ```clojure 90 | at myns$myfn.invoke(myns.clj:12) 91 | at clojure.lang.AFn.applyToHelper(AFn.java:160) 92 | at clojure.lang.AFn.applyTo(AFn.java:144) 93 | at clojure.core$apply.invokeStatic(core.clj:667) 94 | at clojure.core$apply.invoke(core.clj:662) 95 | at memento.caffeine.CaffeineCache$fn__2536.invoke(caffeine.clj:122) 96 | at memento.caffeine.CaffeineCache.cached(caffeine.clj:121) 97 | at memento.mount.UntaggedMountPoint.cached(mount.clj:50) 98 | at memento.mount$bind$fn__2432.doInvoke(mount.clj:119) 99 | at clojure.lang.RestFn.applyTo(RestFn.java:137) 100 | at clojure.lang.AFunction$1.doInvoke(AFunction.java:31) 101 | at clojure.lang.RestFn.invoke(RestFn.java:436) 102 | at myns$myfn.invokeStatic(myns.clj:17) 103 | at myns$myfn.invoke(myns.clj:12) 104 | ``` 105 | 106 | And callstack after: 107 | ```clojure 108 | at myns$myfn.invoke(myns.clj:12) 109 | at clojure.lang.AFn.applyToHelper(AFn.java:160) 110 | at memento.caffeine.CaffeineCache$fn__2052.invoke(caffeine.clj:120) 111 | at memento.caffeine.CaffeineCache.cached(caffeine.clj:119) 112 | at memento.mount.CachedFn.invoke(CachedFn.java:110) 113 | at myns$myfn.invokeStatic(myns.clj:17) 114 | at myns$myfn.invoke(myns.clj:12) 115 | ``` 116 | 117 | From 11 stack frames to 4. 118 | 119 | ## 1.0.37 120 | - remove Guava and replace with Caffeine 121 | - rewrite the readme 122 | - mark Guava namespaces for deprecation 123 | 124 | ## 0.9.36 (2022-03-01) 125 | - add `memento.core/defmemo` 126 | 127 | ## 0.9.35 (2022-02-22) 128 | - add `memento.core/key-fn*` mount setting 129 | 130 | ## 0.9.34 (2022-02-16) 131 | - add support for using meta 132 | 133 | ## 0.9.3 (2021-05-08) 134 | ### Features added 135 | - `memento.core/if-config` 136 | -------------------------------------------------------------------------------- /java/memento/caffeine/SpecialPromise.java: -------------------------------------------------------------------------------- 1 | package memento.caffeine; 2 | 3 | import clojure.lang.ISeq; 4 | import memento.base.EntryMeta; 5 | import memento.base.LockoutMap; 6 | 7 | import java.util.HashSet; 8 | import java.util.concurrent.CountDownLatch; 9 | 10 | /** 11 | * Special promise for use in indirection in Caffeine cache. Do not use otherwise. 12 | * 13 | * This class is NOT threadsafe. The intended use is as a faster CompletableFuture that is 14 | * aware of the thread that made it, so it can detect when same thread is trying to await on it 15 | * and throw to prevent deadlock. 16 | * 17 | * It is also expected that it is created and delivered by the same thread and it is expected that 18 | * any other thread is awaiting result and that only a single attempt is made at delivering the result. 19 | * 20 | * It does not include logic to deal with multiple calls to deliver the promise, as it's optimized for 21 | * a specific use case. 22 | */ 23 | public class SpecialPromise { 24 | 25 | private static final AltResult NIL = new AltResult(null); 26 | private final CountDownLatch d = new CountDownLatch(1); 27 | // these 2 don't need to be thread-safe, because they are only used to check 28 | // if current thread is one that created and started the load on the promise 29 | // so even with non-volatile, check is only true if thread is same as current thread 30 | // so no memory barrier needed 31 | private final HashSet invalidatedIds = new HashSet<>(); 32 | private volatile Thread thread; 33 | private volatile Object result; 34 | 35 | public void init() { 36 | this.thread = Thread.currentThread(); 37 | } 38 | 39 | public Object await(Object stackOverflowContext) throws Throwable { 40 | if (thread == Thread.currentThread()) { 41 | throw new StackOverflowError("Recursive load on key: " + stackOverflowContext); 42 | } 43 | Object r; 44 | if ((r = result) == null) { 45 | d.await(); 46 | r = result; 47 | } 48 | if (r instanceof AltResult) { 49 | Throwable x = ((AltResult) r).value; 50 | if (x == null) { 51 | return null; 52 | } else { 53 | throw x; 54 | } 55 | } else { 56 | return r; 57 | } 58 | } 59 | 60 | private boolean isLockedOut(EntryMeta em) { 61 | try { 62 | return LockoutMap.awaitLockout(em); 63 | } catch (InterruptedException e) { 64 | return true; 65 | } 66 | } 67 | 68 | // Returns true if delivered object is viable 69 | public boolean deliver(Object r) { 70 | if (r instanceof EntryMeta) { 71 | EntryMeta em = (EntryMeta) r; 72 | if (isLockedOut(em) || hasInvalidatedTagId(em)) { 73 | result = EntryMeta.absent; 74 | return false; 75 | } 76 | } 77 | if (result != EntryMeta.absent) { 78 | result = r == null ? NIL : r; 79 | return true; 80 | } 81 | return false; 82 | } 83 | 84 | public void deliverException(Throwable t) { 85 | result = new AltResult(t); 86 | } 87 | 88 | public Object getNow() throws Throwable { 89 | Object r; 90 | if (d.getCount() != 0) { 91 | return EntryMeta.absent; 92 | } 93 | if ((r = result) instanceof AltResult) { 94 | Throwable x = ((AltResult) r).value; 95 | if (x == null) { 96 | return null; 97 | } else { 98 | throw x; 99 | } 100 | } else { 101 | return r == null ? EntryMeta.absent : r; 102 | } 103 | } 104 | 105 | public void invalidate() { 106 | result = EntryMeta.absent; 107 | thread.interrupt(); 108 | } 109 | 110 | public boolean isInvalid() { 111 | return result == EntryMeta.absent; 112 | } 113 | 114 | public void releaseResult() { 115 | d.countDown(); 116 | } 117 | 118 | private boolean hasInvalidatedTagId(EntryMeta entryMeta) { 119 | synchronized (invalidatedIds) { 120 | ISeq s = entryMeta.getTagIdents().seq(); 121 | while (s != null) { 122 | if (invalidatedIds.contains(s.first())) { 123 | return true; 124 | } 125 | s = s.next(); 126 | } 127 | } 128 | return false; 129 | } 130 | 131 | public void addInvalidIds(Iterable ids) { 132 | synchronized (invalidatedIds) { 133 | for (Object id : ids) { 134 | invalidatedIds.add(id); 135 | } 136 | } 137 | } 138 | 139 | private static class AltResult { 140 | Throwable value; 141 | 142 | public AltResult(Throwable value) { 143 | this.value = value; 144 | } 145 | } 146 | 147 | } -------------------------------------------------------------------------------- /src/memento/caffeine.clj: -------------------------------------------------------------------------------- 1 | (ns memento.caffeine 2 | "Caffeine cache implementation." 3 | {:author "Rok Lenarčič"} 4 | (:require [memento.base :as b]) 5 | (:import (java.util.concurrent TimeUnit) 6 | (memento.base Durations CacheKey EntryMeta ICache Segment) 7 | (com.github.benmanes.caffeine.cache Caffeine Weigher Ticker) 8 | (memento.caffeine CaffeineCache_ SecondaryIndex SpecialPromise Expiry) 9 | (memento.mount IMountPoint))) 10 | 11 | (defn create-expiry 12 | "Assumes variable expiry is needed. So either ttl or fade is a function." 13 | [ttl fade ^Expiry cache-expiry] 14 | (let [read-default (some-> (or ttl fade) (Durations/nanos)) 15 | write-default (Durations/nanos (or ttl fade [Long/MAX_VALUE :ns]))] 16 | (reify com.github.benmanes.caffeine.cache.Expiry 17 | (expireAfterCreate [this k v current-time] 18 | (if (instance? SpecialPromise v) 19 | Long/MAX_VALUE 20 | (.expireAfterUpdate this k v current-time Long/MAX_VALUE))) 21 | (expireAfterUpdate [this k v current-time current-duration] 22 | (if-let [ret (.ttl cache-expiry {} (.getArgs ^CacheKey k) v)] 23 | (Durations/nanos ret) 24 | (if-let [ret (.fade cache-expiry {} (.getArgs ^CacheKey k) v)] 25 | (Durations/nanos ret) 26 | write-default))) 27 | (expireAfterRead [this k v current-time current-duration] 28 | (if (instance? SpecialPromise v) 29 | current-duration 30 | ;; if fade is not specified, keep current validity (probably set by ttl) 31 | (if-let [ret (.fade cache-expiry {} (.getArgs ^CacheKey k) v)] 32 | (Durations/nanos ret) 33 | (or read-default current-duration))))))) 34 | 35 | (defn conf->sec-index 36 | "Creates secondary index for evictions" 37 | [{:memento.core/keys [concurrency]}] 38 | (SecondaryIndex. (or concurrency 4))) 39 | 40 | (defn ^Caffeine conf->builder 41 | "Creates and configures common parameters on the builder." 42 | [{:memento.core/keys [initial-capacity size< ttl fade] 43 | :memento.caffeine/keys [weight< removal-listener kv-weight weak-keys weak-values 44 | soft-values refresh stats ticker expiry]}] 45 | (cond-> (Caffeine/newBuilder) 46 | removal-listener (.removalListener (CaffeineCache_/listener removal-listener)) 47 | initial-capacity (.initialCapacity initial-capacity) 48 | weight< (.maximumWeight weight<) 49 | size< (.maximumSize size<) 50 | kv-weight (.weigher 51 | (reify Weigher (weigh [_this k v] 52 | (kv-weight (.getId ^CacheKey k) 53 | (.getArgs ^CacheKey k) 54 | (b/unwrap-meta v))))) 55 | ;; these don't make sense as the caller cannot hold the CacheKey 56 | ;;weak-keys (.weakKeys) 57 | ;; careful around EntryMeta objects 58 | ;; mean that cached values have another wrapper yet again 59 | weak-values (.weakValues) 60 | soft-values (.softValues) 61 | expiry (.expireAfter (create-expiry ttl fade expiry)) 62 | (and (not expiry) ttl) (.expireAfterWrite (Durations/nanos ttl) TimeUnit/NANOSECONDS) 63 | (and (not expiry) fade) (.expireAfterAccess (Durations/nanos fade) TimeUnit/NANOSECONDS) 64 | ;; not currently used because we don't build a loading cache 65 | refresh (.refreshAfterWrite (Durations/nanos refresh) TimeUnit/NANOSECONDS) 66 | ticker (.ticker (proxy [Ticker] [] (read [] (ticker)))) 67 | stats (.recordStats))) 68 | 69 | (defn assoc-imm-val! 70 | "If cached value is a completable future with immediately available value, assoc it to transient." 71 | [transient-m k v xf] 72 | (let [cv (if (instance? SpecialPromise v) 73 | (.getNow ^SpecialPromise v) 74 | v)] 75 | (if (identical? cv b/absent) 76 | transient-m 77 | (assoc! transient-m k (xf cv))))) 78 | 79 | ;;;;;;;;;;;;;; 80 | ; ->key-fn take 2 args f and key 81 | (defrecord CaffeineCache [conf ^CaffeineCache_ caffeine-cache] 82 | ICache 83 | (conf [this] conf) 84 | (cached [this segment args] 85 | (.cached caffeine-cache segment args)) 86 | (ifCached [this segment args] 87 | (.ifCached caffeine-cache segment args)) 88 | (invalidate [this segment] 89 | (.invalidate caffeine-cache ^Segment segment) 90 | this) 91 | (invalidate [this segment args] (.invalidate caffeine-cache ^Segment segment args) 92 | this) 93 | (invalidateAll [this] (.invalidateAll caffeine-cache) this) 94 | (invalidateIds [this ids] 95 | (.invalidateIds caffeine-cache ids) 96 | this) 97 | (addEntries [this segment args-to-vals] 98 | (.addEntries caffeine-cache segment args-to-vals) 99 | this) 100 | (asMap [this] (persistent! 101 | (reduce (fn [m [k v]] (assoc-imm-val! m k v b/unwrap-meta)) 102 | (transient {}) 103 | (.asMap caffeine-cache)))) 104 | (asMap [this segment] 105 | (persistent! 106 | (reduce (fn [m [^CacheKey k v]] 107 | (if (= (.getId segment) (.getId k)) (assoc-imm-val! m (.getArgs k) v b/unwrap-meta) 108 | m)) 109 | (transient {}) 110 | (.asMap caffeine-cache))))) 111 | 112 | (defmethod b/new-cache :memento.core/caffeine [conf] 113 | (->CaffeineCache conf (CaffeineCache_. 114 | (conf->builder conf) 115 | (:memento.core/key-fn conf) 116 | (:memento.core/ret-fn conf) 117 | (:memento.core/ret-ex-fn conf) 118 | (conf->sec-index conf)))) 119 | 120 | (defn stats 121 | "Return caffeine stats for the cache if it is a caffeine Cache. 122 | 123 | Takes a memoized fn or a Cache instance as a parameter. 124 | 125 | Returns com.github.benmanes.caffeine.cache.stats.CacheStats" 126 | [fn-or-cache] 127 | (if (instance? ICache fn-or-cache) 128 | (when (instance? CaffeineCache fn-or-cache) 129 | (.stats ^CaffeineCache_ (:caffeine-cache fn-or-cache))) 130 | (stats (.mountedCache ^IMountPoint fn-or-cache)))) 131 | 132 | (defn to-data [cache] 133 | (when-let [caffeine (:caffeine-cache cache)] 134 | (persistent! 135 | (reduce (fn [m [^CacheKey k v]] (assoc-imm-val! m 136 | [(.getId k) (.getArgs k)] 137 | v 138 | #(if (and (instance? EntryMeta %) (nil? (.getV ^EntryMeta %))) 139 | nil %))) 140 | (transient {}) 141 | (.asMap ^CaffeineCache_ caffeine))))) 142 | 143 | (defn load-data [cache data-map] 144 | (.loadData ^CaffeineCache_ (:caffeine-cache cache) data-map) 145 | cache) 146 | -------------------------------------------------------------------------------- /src/memento/mount.clj: -------------------------------------------------------------------------------- 1 | (ns memento.mount 2 | "Mount points, they serve as glue between a cache that can house entries from 3 | multiple functions and the individual functions." 4 | {:author "Rok Lenarčič"} 5 | (:require [memento.base :as base] 6 | [memento.config :as config]) 7 | (:import (clojure.lang AFn ISeq MultiFn) 8 | (memento.base ICache Segment) 9 | (memento.mount Cached CachedFn CachedMultiFn IMountPoint))) 10 | 11 | (def ^:dynamic *caches* "Contains map of mount point to cache instance" {}) 12 | (def tags "Map tag to mount-point" (atom {})) 13 | 14 | (def configuration-props [config/key-fn config/ret-fn config/seed config/tags 15 | config/evt-fn config/id config/key-fn* config/ret-ex-fn]) 16 | 17 | (defn assoc-cache-tags 18 | "Add Mount Point ref to tag index" 19 | [index cache-tags ref] 20 | (reduce #(update %1 %2 (fnil conj #{}) ref) index cache-tags)) 21 | 22 | (defn dissoc-cache-tags 23 | "Remove Mount Point ref from tag index" 24 | [index ref] 25 | (reduce-kv #(assoc %1 %2 (disj %3 ref)) {} index)) 26 | 27 | (deftype TagsUnloader [cache-mount] 28 | Runnable 29 | (run [this] 30 | (swap! tags dissoc-cache-tags cache-mount) 31 | (alter-var-root #'*caches* dissoc cache-mount) 32 | nil)) 33 | 34 | (defrecord UntaggedMountPoint [^ICache cache ^Segment segment evt-handler] 35 | IMountPoint 36 | (asMap [this] (.asMap cache segment)) 37 | (cached [this args] (.cached cache segment args)) 38 | (ifCached [this args] (.ifCached cache segment args)) 39 | (getTags [this] []) 40 | (handleEvent [this evt] (evt-handler this evt)) 41 | (invalidate [this args] (.invalidate cache segment args)) 42 | (invalidateAll [this] (.invalidate cache segment)) 43 | (mountedCache [this] cache) 44 | (addEntries [this args-to-vals] (.addEntries cache segment args-to-vals)) 45 | (segment [this] segment)) 46 | 47 | (defrecord TaggedMountPoint [tags ^Segment segment evt-handler] 48 | IMountPoint 49 | (asMap [this] (.asMap ^ICache (*caches* this base/no-cache) segment)) 50 | (cached [this args] (.cached ^ICache (*caches* this base/no-cache) segment args)) 51 | (ifCached [this args] (.ifCached ^ICache (*caches* this base/no-cache) segment args)) 52 | (getTags [this] tags) 53 | (handleEvent [this evt] (evt-handler this evt)) 54 | (invalidate [this args] (.invalidate ^ICache (*caches* this base/no-cache) segment args)) 55 | (invalidateAll [this] (.invalidate ^ICache (*caches* this base/no-cache) segment)) 56 | (mountedCache [this] (*caches* this base/no-cache)) 57 | (addEntries [this args-to-vals] 58 | (.addEntries ^ICache (*caches* this base/no-cache) segment args-to-vals)) 59 | (segment [this] segment)) 60 | 61 | (defn mounted-cache [^IMountPoint mp] (.mountedCache mp)) 62 | 63 | (defn reify-mount-conf 64 | "Transform user given mount-conf to a canonical form of a map." 65 | [mount-conf] 66 | (if (map? mount-conf) 67 | mount-conf 68 | {config/tags ((if (sequential? mount-conf) vec vector) mount-conf)})) 69 | 70 | (defn wrap-fn 71 | [f ret-fn ret-ex-fn] 72 | (cond 73 | (and ret-fn ret-ex-fn) (fn [& args] 74 | (try (ret-fn args (AFn/applyToHelper f args)) 75 | (catch Throwable t (throw (ret-ex-fn args t))))) 76 | ret-fn (fn [& args] (ret-fn args (AFn/applyToHelper f args))) 77 | ret-ex-fn (fn [& args] 78 | (try (AFn/applyToHelper f args) 79 | (catch Throwable t (throw (ret-ex-fn args t))))) 80 | :else f)) 81 | 82 | (defn create-mount 83 | "Create mount record by specified map conf" 84 | [f cache mount-conf] 85 | (let [key-fn (or (config/key-fn mount-conf) 86 | (when-let [base (config/key-fn* mount-conf)] 87 | (fn [args] (AFn/applyToHelper base (if (instance? ISeq args) args (seq args))))) 88 | identity) 89 | evt-fn (config/evt-fn mount-conf (fn [_ _] nil)) 90 | f* (wrap-fn f (config/ret-fn mount-conf) (config/ret-ex-fn mount-conf)) 91 | segment (Segment. f* key-fn (mount-conf config/id f) mount-conf)] 92 | (if-let [t (config/tags mount-conf)] 93 | (let [wrapped-t (if (sequential? t) t (vector t)) 94 | mp (->TaggedMountPoint wrapped-t segment evt-fn)] 95 | (alter-var-root #'*caches* assoc mp cache) 96 | (swap! tags assoc-cache-tags wrapped-t mp) 97 | mp) 98 | (->UntaggedMountPoint cache segment evt-fn)))) 99 | 100 | (defn bind 101 | "Bind a cache to a fn or var. Internal function." 102 | [fn-or-var mount-conf cache] 103 | (if (var? fn-or-var) 104 | (let [mount-conf (-> mount-conf 105 | reify-mount-conf 106 | (update config/id #(or % (.intern (str fn-or-var)))))] 107 | (alter-var-root fn-or-var bind mount-conf cache)) 108 | (let [mount-conf (reify-mount-conf mount-conf) 109 | constructor (if (instance? MultiFn fn-or-var) 110 | #(CachedMultiFn. (str (config/id mount-conf)) %1 %2 %3 %4) 111 | #(CachedFn. %1 %2 %3 %4)) 112 | stacking (if (instance? Cached fn-or-var) (config/bind-mode mount-conf :new) :none) 113 | ^IMountPoint cache-mount (case stacking 114 | :new (create-mount (.getOriginalFn ^Cached fn-or-var) cache mount-conf) 115 | :keep (.getMp ^Cached fn-or-var) 116 | (:none :stack) (create-mount fn-or-var cache mount-conf)) 117 | reload-guard (when (and config/reload-guards? (config/tags mount-conf) (not= :keep stacking)) 118 | (doto (Object.) 119 | (IMountPoint/register (->TagsUnloader cache-mount)))) 120 | f (case stacking 121 | :keep fn-or-var 122 | (:new :stack) (constructor reload-guard cache-mount (meta fn-or-var) (.getOriginalFn ^Cached fn-or-var)) 123 | :none (constructor reload-guard cache-mount (meta fn-or-var) fn-or-var))] 124 | (.addEntries (.getMp ^Cached f) (config/seed mount-conf {})) 125 | f))) 126 | 127 | (defn mount-point 128 | "Return active mount point from the object's meta." 129 | [obj] 130 | (when (instance? IMountPoint obj) obj)) 131 | 132 | (defn update-existing 133 | "Convenience function. Updates ks's that are present with the provided update fn." 134 | [m ks update-fn] 135 | (reduce #(if-let [kv (find %1 %2)] (assoc %1 %2 (update-fn (val kv))) %1) m ks)) 136 | 137 | (defn alter-caches-mapping 138 | "Internal function. Modifies entire tagged cache map with the provided function. 139 | Applies the function as (fn [*caches* refs & other-update-fn-args])" 140 | [tag update-fn & update-fn-args] 141 | (let [refs (get @tags tag []) 142 | update-fn #(apply update-fn % refs update-fn-args)] 143 | (if (.getThreadBinding #'*caches*) 144 | (var-set #'*caches* (update-fn *caches*)) 145 | (alter-var-root #'*caches* update-fn)))) 146 | -------------------------------------------------------------------------------- /src/memento/config.clj: -------------------------------------------------------------------------------- 1 | (ns memento.config 2 | "Memoization library config. 3 | 4 | Contains global settings that manipulate the cache mechanisms. 5 | See doc strings. 6 | 7 | Contains conf settings as vars. 8 | 9 | Also contains documented definitions of standard options of cache config." 10 | {:author "Rok Lenarčič"} 11 | (:refer-clojure :exclude [type]) 12 | (:import (java.util.concurrent TimeUnit))) 13 | 14 | (def ^:redef enabled? 15 | "If false, then all cache attach operations create a cache that does no 16 | caching (changing this value doesn't affect caches already created). 17 | 18 | Initially has the value of java property `memento.enabled` (defaulting to true)." 19 | (Boolean/valueOf (System/getProperty "memento.enabled" "true"))) 20 | 21 | (def ^:redef reload-guards? 22 | "If true, then whenever a function cached with tags is garbage collected (e.g. after a namespace reload in REPL), 23 | a cleanup is done of global tags map. Can be turned off if you don't intend to reload namespaces or do other 24 | actions that would GC cached function instances. 25 | 26 | Initially has the value of java property `memento.reloadable` (defaulting to true)." 27 | (Boolean/valueOf (System/getProperty "memento.reloadable" "true"))) 28 | 29 | (def ^:dynamic *default-type* "Default cache type." :memento.core/none) 30 | 31 | (def type 32 | "Cache setting, type of cache or region that will be instantiated, a keyword. 33 | 34 | The library has two built-ins: 35 | - memento.core/none 36 | - memento.core/caffeine 37 | 38 | If not specified the caches created default to *default-type*." 39 | :memento.core/type) 40 | 41 | (def bind-mode 42 | "Function bind setting, defaults to :new. It governs what the bind will do if you try to bind 43 | a cache to a function that is already cached, e.g. what happens when memo is called multiple times 44 | on same Var. Options are: 45 | - :keep, keeps old cache binding 46 | - :new, keeps the new cache binding 47 | - :stack, stacks the caches, so the new binding wraps the older cached function" 48 | :memento.core/bind-mode) 49 | 50 | (def key-fn 51 | "Cache and function bind setting, a function to be used to calculate the cache key (fn [f-args] key). 52 | 53 | Cache key affects what is considered the 'same' argument list for a function and it will affect caching in that manner. 54 | 55 | If both function bind and cache have this setting, then function bind key-fn is applied first. 56 | 57 | It's a function of 1 argument, the seq of function arguments. If not provided it defaults to identity." 58 | :memento.core/key-fn) 59 | 60 | (def key-fn* 61 | "Function bind setting, works same as key-fn but the provided function will receive all 62 | arguments that the original function does. If both key-fn and key-fn* are provided, key-fn is used." 63 | :memento.core/key-fn*) 64 | 65 | (def ret-fn 66 | "Cache and function bind setting, a function that is ran to process the return of the function, before it's memoized, 67 | (fn [fn-args ret-value] transformed-value). 68 | 69 | It can provide some generic transformation facility, but more importantly, it can wrap specific return 70 | values in 'do-not-cache' object, that prevents caching or wrap with tagged IDs." 71 | :memento.core/ret-fn) 72 | 73 | (def evt-fn 74 | "Function bind setting, a function that is invoked when any event is fired at the function. 75 | 76 | (fn [mnt-point event] void) 77 | 78 | Useful generally to push data into the related cache, the mnt-point parameter implement MountPoint protocol 79 | so you can invoke memo-add! and such on it. The event can be any data, it's probably best to come up with 80 | a format that enables the functions that receive the event to be able to tell them apart." 81 | :memento.core/evt-fn) 82 | 83 | (def seed 84 | "Function bind setting, a map of cache keys to values that will be preloaded when cache is bound." 85 | :memento.core/seed) 86 | 87 | (def ^:deprecated guava 88 | "DEPRECATED: Cache setting value, now points to caffeine implementation" 89 | :memento.core/caffeine) 90 | 91 | (def caffeine 92 | "Cache setting value, type name of Caffeine cache implementation" 93 | :memento.core/caffeine) 94 | 95 | (def none 96 | "Cache setting value, type name of noop cache implementation" 97 | :memento.core/none) 98 | 99 | (def ^:deprecated concurrency 100 | "DEPRECATED: it does nothing in Caffeine implementation" 101 | :memento.core/concurrency) 102 | 103 | (def initial-capacity 104 | "Cache setting, supported by: caffeine, an int. 105 | 106 | Sets the minimum total size for the internal hash tables. Providing a large enough estimate 107 | at construction time avoids the need for expensive resizing operations later, 108 | but setting this value unnecessarily high wastes memory." 109 | :memento.core/initial-capacity) 110 | 111 | (def size< 112 | "Cache setting, supported by: caffeine, a long. 113 | 114 | Specifies the maximum number of entries the cache may contain. Some implementations might evict entries 115 | even before the number of entries reaches the limit." 116 | :memento.core/size<) 117 | 118 | (def ttl 119 | "Cache and Function bind setting, a duration. 120 | 121 | Specifies that each entry should be automatically removed from the cache once a duration 122 | has elapsed after the entry's creation, or the most recent replacement of its value via a put. 123 | 124 | Duration is specified by a number (of seconds) or a vector of a number and unit keyword. 125 | So ttl of 10 or [10 :s] is the same. See 'timeunits' var." 126 | :memento.core/ttl) 127 | 128 | (def fade 129 | "Cache and Function bind setting, a duration. 130 | 131 | Specifies that each entry should be automatically removed from the cache once a fixed duration 132 | has elapsed after the entry's creation, the most recent replacement of its value, or its last access. 133 | Access time is reset by all cache read and write operations. 134 | 135 | Duration is specified by a number (of seconds) or a vector of a number and unit keyword. 136 | So fade of 10 or [10 :s] is the same. See 'timeunits' var." 137 | :memento.core/fade) 138 | 139 | (def tags 140 | "Function bind setting. 141 | 142 | List of tags for this memoized bind." 143 | :memento.core/tags) 144 | 145 | (def id 146 | "Function bind setting. 147 | 148 | Id of the function bind. If you're memoizing a Var, this defaults to stringified var name, 149 | otherwise the ID is the function itself. 150 | 151 | This is useful to specify when you're using Cache implementation that stores data outside JVM, 152 | as they often need a name for each function's cache." 153 | :memento.core/id) 154 | 155 | (def timeunits 156 | "Timeunits keywords, corresponds with Durations class." 157 | {:ns TimeUnit/NANOSECONDS 158 | :us TimeUnit/MICROSECONDS 159 | :ms TimeUnit/MILLISECONDS 160 | :s TimeUnit/SECONDS 161 | :m TimeUnit/MINUTES 162 | :h TimeUnit/HOURS 163 | :d TimeUnit/DAYS}) 164 | 165 | (def cache 166 | "The key extracted from object/var meta and used as cache configuration when 167 | 1-arg memo is called or ns-scan based mounting is performed." 168 | :memento.core/cache) 169 | 170 | (def mount 171 | "The key extracted from object/var meta and used as mount configuration when 172 | 1-arg memo is called or ns-scan based mounting is performed." 173 | :memento.core/mount) 174 | 175 | (def ret-ex-fn 176 | "Cache and function bind setting, a function that is ran to process the throwable thrown by the function, 177 | (fn [fn-args throwable] throwable)." 178 | :memento.core/ret-ex-fn) 179 | -------------------------------------------------------------------------------- /doc/performance.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | Performance is not a dedicated goal of this library, but here's some numbers: 4 | 5 |  6 | 7 |  8 | 9 | ```clojure 10 | ; memoize is not thread-safe and doesn't have any features 11 | (def f-memoize (memoize identity)) 12 | ; clojure.core.memoize 13 | (def f-core-memo (ccm/memo identity)) 14 | ; memento 15 | (def f-memento (m/memo identity {::m/type ::m/caffeine})) 16 | ; memento caffeine variable expiry 17 | (def f-memento-var (m/memo identity {::m/type ::m/caffeine ::m/expiry memento.caffeine.config/meta-expiry})) 18 | ; memento light caffeine 19 | (def f-light-memento (m/memo identity {::m/type ::m/light-caffeine})) 20 | ``` 21 | ## Memoize 22 | 23 | #### All hits 24 | ```text 25 | (cc/bench (f-memoize 1)) 26 | Evaluation count : 2911575540 in 60 samples of 48526259 calls. 27 | Execution time mean : 18,520670 ns 28 | Execution time std-deviation : 0,632964 ns 29 | Execution time lower quantile : 18,041806 ns ( 2,5%) 30 | Execution time upper quantile : 20,272312 ns (97,5%) 31 | Overhead used : 1,997090 ns 32 | 33 | Found 2 outliers in 60 samples (3,3333 %) 34 | low-severe 2 (3,3333 %) 35 | Variance from outliers : 20,6200 % Variance is moderately inflated by outliers 36 | 37 | ``` 38 | 39 | #### 1M misses (426ns per miss) 40 | ```text 41 | (cc/bench (let [f-memoize (memoize identity)] 42 | (reduce #(f-memoize %2) (range 1000000)))) 43 | Evaluation count : 180 in 60 samples of 3 calls. 44 | Execution time mean : 426,691729 ms 45 | Execution time std-deviation : 31,649211 ms 46 | Execution time lower quantile : 407,433346 ms ( 2,5%) 47 | Execution time upper quantile : 500,285216 ms (97,5%) 48 | Overhead used : 1,997090 ns 49 | 50 | Found 9 outliers in 60 samples (15,0000 %) 51 | low-severe 5 (8,3333 %) 52 | low-mild 4 (6,6667 %) 53 | Variance from outliers : 55,1467 % Variance is severely inflated by outliers 54 | ``` 55 | 56 | ## Clojure Core Memoize 57 | 58 | #### All hits 59 | 60 | ```text 61 | (cc/bench (f-core-memo 1)) 62 | Evaluation count : 329229720 in 60 samples of 5487162 calls. 63 | Execution time mean : 180,803852 ns 64 | Execution time std-deviation : 3,880666 ns 65 | Execution time lower quantile : 177,830691 ns ( 2,5%) 66 | Execution time upper quantile : 189,061520 ns (97,5%) 67 | Overhead used : 1,997090 ns 68 | 69 | Found 6 outliers in 60 samples (10,0000 %) 70 | low-severe 3 (5,0000 %) 71 | low-mild 3 (5,0000 %) 72 | Variance from outliers : 9,4347 % Variance is slightly inflated by outliers 73 | ``` 74 | 75 | #### 1M misses (778 ns per miss) 76 | 77 | ```text 78 | (cc/bench (let [f-core-memo (ccm/memo identity)] 79 | (reduce #(f-core-memo %2) (range 1000000)))) 80 | Evaluation count : 120 in 60 samples of 2 calls. 81 | Execution time mean : 778,758053 ms 82 | Execution time std-deviation : 58,068726 ms 83 | Execution time lower quantile : 717,950541 ms ( 2,5%) 84 | Execution time upper quantile : 947,641405 ms (97,5%) 85 | Overhead used : 1,997090 ns 86 | 87 | Found 6 outliers in 60 samples (10,0000 %) 88 | low-severe 4 (6,6667 %) 89 | low-mild 2 (3,3333 %) 90 | Variance from outliers : 55,1627 % Variance is severely inflated by outliers 91 | ``` 92 | 93 | #### 1M misses for size 100 LRU cache (1811 ns per miss) 94 | 95 | ```text 96 | (cc/bench (let [f-core-memo (ccm/lru identity :lru/threshold 100)] 97 | (reduce #(f-core-memo %2) (range 1000000)))) 98 | Evaluation count : 60 in 60 samples of 1 calls. 99 | Execution time mean : 1,811235 sec 100 | Execution time std-deviation : 23,960121 ms 101 | Execution time lower quantile : 1,773504 sec ( 2,5%) 102 | Execution time upper quantile : 1,866470 sec (97,5%) 103 | Overhead used : 1,997090 ns 104 | 105 | Found 2 outliers in 60 samples (3,3333 %) 106 | low-severe 2 (3,3333 %) 107 | Variance from outliers : 1,6389 % Variance is slightly inflated by outliers 108 | 109 | ``` 110 | 111 | ## Memento 112 | 113 | #### All hits 114 | 115 | ```text 116 | (cc/bench (f-memento 1)) 117 | 118 | Evaluation count : 854138220 in 60 samples of 14235637 calls. 119 | Execution time mean : 70,745055 ns 120 | Execution time std-deviation : 2,570125 ns 121 | Execution time lower quantile : 68,659819 ns ( 2,5%) 122 | Execution time upper quantile : 74,128774 ns (97,5%) 123 | Overhead used : 1,970580 ns 124 | 125 | Found 2 outliers in 60 samples (3,3333 %) 126 | low-severe 1 (1,6667 %) 127 | low-mild 1 (1,6667 %) 128 | Variance from outliers : 22,2591 % Variance is moderately inflated by outliers 129 | 130 | 131 | ``` 132 | 133 | #### 1M misses (474 ns per miss) 134 | 135 | ```text 136 | (cc/bench (let [f-memento (m/memo identity {::m/type ::m/caffeine})] 137 | (reduce #(f-memento %2) (range 1000000)))) 138 | Evaluation count : 120 in 60 samples of 2 calls. 139 | Execution time mean : 474,650866 ms 140 | Execution time std-deviation : 76,082064 ms 141 | Execution time lower quantile : 365,465019 ms ( 2,5%) 142 | Execution time upper quantile : 641,739223 ms (97,5%) 143 | Overhead used : 1,992837 ns 144 | 145 | ``` 146 | 147 | #### 1M misses for size 100 LRU cache (338 ns per miss) 148 | 149 | ```text 150 | (cc/bench (let [f-memento (m/memo identity {::m/size< 100 ::m/type ::m/caffeine})] 151 | (reduce #(f-memento %2) (range 1000000)))) 152 | Evaluation count : 180 in 60 samples of 3 calls. 153 | Execution time mean : 338,339882 ms 154 | Execution time std-deviation : 15,865012 ms 155 | Execution time lower quantile : 321,764748 ms ( 2,5%) 156 | Execution time upper quantile : 370,249429 ms (97,5%) 157 | Overhead used : 1,970580 ns 158 | 159 | Found 4 outliers in 60 samples (6,6667 %) 160 | low-severe 3 (5,0000 %) 161 | low-mild 1 (1,6667 %) 162 | Variance from outliers : 33,5491 % Variance is moderately inflated by outliers 163 | 164 | 165 | ``` 166 | 167 | ## Memento Variable Expiry 168 | 169 | #### All hits 170 | 171 | ```text 172 | (cc/bench (f-memento-var 1)) 173 | 174 | Evaluation count : 453412980 in 60 samples of 7556883 calls. 175 | Execution time mean : 132,501700 ns 176 | Execution time std-deviation : 2,015071 ns 177 | Execution time lower quantile : 130,326931 ns ( 2,5%) 178 | Execution time upper quantile : 134,890796 ns (97,5%) 179 | Overhead used : 1,978672 ns 180 | 181 | ``` 182 | 183 | #### 1M misses (526 ns per miss) 184 | 185 | ```text 186 | (cc/bench (let [f-memento-var (m/memo identity {::m/type ::m/caffeine ::m/expiry memento.caffeine.config/meta-expiry})] 187 | (reduce #(f-memento-var %2) (range 1000000)))) 188 | Evaluation count : 120 in 60 samples of 2 calls. 189 | Execution time mean : 526,197766 ms 190 | Execution time std-deviation : 59,110910 ms 191 | Execution time lower quantile : 426,811124 ms ( 2,5%) 192 | Execution time upper quantile : 644,451645 ms (97,5%) 193 | Overhead used : 1,978672 ns 194 | 195 | ``` 196 | 197 | #### 1M misses for size 100 LRU cache (387 ns per miss) 198 | 199 | ```text 200 | (cc/bench (let [f-memento-var (m/memo identity {::m/size< 100 ::m/type ::m/caffeine ::m/expiry memento.caffeine.config/meta-expiry})] 201 | (reduce #(f-memento-var %2) (range 1000000)))) 202 | Evaluation count : 180 in 60 samples of 3 calls. 203 | Execution time mean : 423,554590 ms 204 | Execution time std-deviation : 7,825220 ms 205 | Execution time lower quantile : 414,372683 ms ( 2,5%) 206 | Execution time upper quantile : 435,451863 ms (97,5%) 207 | Overhead used : 1,978672 ns 208 | 209 | ``` -------------------------------------------------------------------------------- /java/memento/caffeine/CaffeineCache_.java: -------------------------------------------------------------------------------- 1 | package memento.caffeine; 2 | 3 | import clojure.lang.*; 4 | import com.github.benmanes.caffeine.cache.Cache; 5 | import com.github.benmanes.caffeine.cache.Caffeine; 6 | import com.github.benmanes.caffeine.cache.RemovalListener; 7 | import com.github.benmanes.caffeine.cache.stats.CacheStats; 8 | import memento.base.CacheKey; 9 | import memento.base.EntryMeta; 10 | import memento.base.LockoutMap; 11 | import memento.base.Segment; 12 | 13 | import java.util.*; 14 | import java.util.concurrent.ConcurrentHashMap; 15 | import java.util.concurrent.ConcurrentMap; 16 | import java.util.function.BiFunction; 17 | 18 | public class CaffeineCache_ { 19 | 20 | private final BiFunction keyFn; 21 | 22 | private final SecondaryIndex secIndex; 23 | private final IFn retFn; 24 | 25 | private final IFn retExFn; 26 | 27 | private final Cache delegate; 28 | 29 | private final Set loads = ConcurrentHashMap.newKeySet(); 30 | 31 | public CaffeineCache_(Caffeine builder, final IFn keyFn, final IFn retFn, final IFn retExFn, SecondaryIndex secIndex) { 32 | this.keyFn = keyFn == null ? 33 | (segment, args) -> new CacheKey(segment.getId(), segment.getKeyFn().invoke(args)) : 34 | (segment, args) -> new CacheKey(segment.getId(), keyFn.invoke(segment.getKeyFn().invoke(args))); 35 | this.retFn = retFn; 36 | this.delegate = builder.build(); 37 | this.secIndex = secIndex; 38 | this.retExFn = retExFn; 39 | } 40 | 41 | private void initLoad(SpecialPromise promise) { 42 | promise.init(); 43 | loads.add(promise); 44 | } 45 | 46 | public Object cached(Segment segment, ISeq args) throws Throwable { 47 | CacheKey key = keyFn.apply(segment, args); 48 | do { 49 | SpecialPromise p = new SpecialPromise(); 50 | // check for ongoing load 51 | Object cached = delegate.asMap().putIfAbsent(key, p); 52 | if (cached == null) { 53 | try { 54 | initLoad(p); 55 | // calculate value 56 | Object result = AFn.applyToHelper(segment.getF(), args); 57 | if (retFn != null) { 58 | result = retFn.invoke(args, result); 59 | } 60 | if (!p.deliver(result)) { 61 | // The SpecialPromise was invalidated, restart the process 62 | delegate.asMap().remove(key, p); 63 | continue; 64 | } 65 | // if valid add to secondary index 66 | secIndex.add(key, result); 67 | if (result instanceof EntryMeta && ((EntryMeta) result).isNoCache()) { 68 | delegate.asMap().remove(key, p); 69 | } else { 70 | delegate.asMap().replace(key, p, result == null ? EntryMeta.NIL : result); 71 | } 72 | return EntryMeta.unwrap(result); 73 | } catch (Throwable t) { 74 | delegate.asMap().remove(key, p); 75 | if (!p.isInvalid()) { 76 | p.deliverException(retExFn == null ? t : (Throwable) retExFn.invoke(args, t)); 77 | throw t; 78 | } 79 | } finally { 80 | p.releaseResult(); 81 | loads.remove(p); 82 | } 83 | } else { 84 | // join into ongoing load 85 | if (cached instanceof SpecialPromise) { 86 | SpecialPromise sp = (SpecialPromise) cached; 87 | Object ret = sp.await(key); 88 | if (ret != EntryMeta.absent && !LockoutMap.awaitLockout(ret)) { 89 | // if not invalidated, return the value 90 | return EntryMeta.unwrap(ret); 91 | } 92 | } else { 93 | if (!LockoutMap.awaitLockout(cached)) { 94 | // if not invalidated, return the value 95 | return EntryMeta.unwrap(cached); 96 | } 97 | } 98 | // else try to initiate load again 99 | } 100 | } while (true); 101 | } 102 | 103 | public Object ifCached(Segment segment, ISeq args) throws Throwable { 104 | CacheKey key = keyFn.apply(segment, args); 105 | Object v = delegate.getIfPresent(key); 106 | Object absent = EntryMeta.absent; 107 | if (v == null) { 108 | return absent; 109 | } else if (v instanceof SpecialPromise) { 110 | SpecialPromise p = (SpecialPromise) v; 111 | Object ret = p.getNow(); 112 | if (ret == absent || LockoutMap.awaitLockout(ret)) { 113 | return absent; 114 | } else { 115 | return EntryMeta.unwrap(ret); 116 | } 117 | } else { 118 | return LockoutMap.awaitLockout(v) ? absent : EntryMeta.unwrap(v); 119 | } 120 | } 121 | 122 | public void invalidate(Segment segment) { 123 | final Iterator> iter = delegate.asMap().entrySet().iterator(); 124 | while (iter.hasNext()) { 125 | Map.Entry it = iter.next(); 126 | if (it.getKey().getId().equals(segment.getId())) { 127 | Object v = it.getValue(); 128 | if (v instanceof SpecialPromise) { 129 | ((SpecialPromise) v).invalidate(); 130 | } 131 | iter.remove(); 132 | } 133 | } 134 | } 135 | 136 | public void invalidate(Segment segment, ISeq args) { 137 | Object v = delegate.asMap().remove(keyFn.apply(segment, args)); 138 | if (v instanceof SpecialPromise) { 139 | ((SpecialPromise) v).invalidate(); 140 | } 141 | } 142 | 143 | public void invalidateAll() { 144 | delegate.invalidateAll(); 145 | } 146 | 147 | public void invalidateIds(Iterable ids) { 148 | HashSet keys = new HashSet<>(); 149 | for (Object id : ids) { 150 | secIndex.drainKeys(id, keys::add); 151 | } 152 | ConcurrentMap map = delegate.asMap(); 153 | for (CacheKey k : keys) { 154 | Object removed = map.remove(k); 155 | if (removed instanceof SpecialPromise) { 156 | ((SpecialPromise) removed).invalidate(); 157 | } 158 | } 159 | loads.forEach(row -> row.addInvalidIds(ids)); 160 | } 161 | 162 | public void addEntries(Segment segment, IPersistentMap argsToVals) { 163 | for (Object o : argsToVals) { 164 | MapEntry entry = (MapEntry) o; 165 | CacheKey key = keyFn.apply(segment, RT.seq(entry.getKey())); 166 | Object val = entry.getValue(); 167 | secIndex.add(key, val); 168 | delegate.put(key, val == null ? EntryMeta.NIL : val); 169 | } 170 | } 171 | 172 | public ConcurrentMap asMap() { 173 | return delegate.asMap(); 174 | } 175 | 176 | public CacheStats stats() { 177 | return delegate.stats(); 178 | } 179 | 180 | public void loadData(Map map) { 181 | map.forEach((Object k, Object v) -> { 182 | List list = (List) k; 183 | CacheKey key = new CacheKey(list.get(0), list.get(1)); 184 | secIndex.add(key, v); 185 | delegate.put(key, v == null ? EntryMeta.NIL : v); 186 | }); 187 | } 188 | 189 | public static RemovalListener listener(IFn removalListener) { 190 | return (k, v, removalCause) -> { 191 | if (!(v instanceof SpecialPromise)) { 192 | removalListener.invoke(k.getId(), k.getArgs(), v instanceof EntryMeta ? ((EntryMeta) v).getV() : v, removalCause); 193 | } 194 | }; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /java/memento/multi/TieredCache.java: -------------------------------------------------------------------------------- 1 | package memento.multi; 2 | 3 | import clojure.lang.ArraySeq; 4 | import clojure.lang.IFn; 5 | import clojure.lang.IPersistentMap; 6 | import clojure.lang.ISeq; 7 | import memento.base.ICache; 8 | import memento.base.Segment; 9 | 10 | public class TieredCache extends MultiCache { 11 | public TieredCache(ICache cache, ICache upstream, IPersistentMap conf, Object absent) { 12 | super(cache, upstream, conf, absent); 13 | } 14 | 15 | @Override 16 | public Object cached(Segment segment, ISeq args) { 17 | return cache.cached(segment.withFn(new AskUpstream(segment)), args); 18 | } 19 | 20 | private class AskUpstream implements IFn { 21 | 22 | private final Segment segment; 23 | 24 | public AskUpstream(Segment segment) { 25 | this.segment = segment; 26 | } 27 | 28 | @Override 29 | public Object call() { 30 | return upstream.cached(segment, ArraySeq.create()); 31 | } 32 | 33 | @Override 34 | public void run() { 35 | upstream.cached(segment, ArraySeq.create()); 36 | } 37 | 38 | @Override 39 | public Object invoke() { 40 | return upstream.cached(segment, ArraySeq.create()); 41 | } 42 | 43 | @Override 44 | public Object invoke(Object arg1) { 45 | return upstream.cached(segment, ArraySeq.create(arg1)); 46 | } 47 | 48 | @Override 49 | public Object invoke(Object arg1, Object arg2) { 50 | return upstream.cached(segment, ArraySeq.create(arg1, arg2)); 51 | } 52 | 53 | @Override 54 | public Object invoke(Object arg1, Object arg2, Object arg3) { 55 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3)); 56 | } 57 | 58 | @Override 59 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 60 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4)); 61 | } 62 | 63 | @Override 64 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 65 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5)); 66 | } 67 | 68 | @Override 69 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 70 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6)); 71 | } 72 | 73 | @Override 74 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 75 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7)); 76 | } 77 | 78 | @Override 79 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 80 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)); 81 | } 82 | 83 | @Override 84 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 85 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)); 86 | } 87 | 88 | @Override 89 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 90 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); 91 | } 92 | 93 | @Override 94 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 95 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11)); 96 | } 97 | 98 | @Override 99 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 100 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12)); 101 | } 102 | 103 | @Override 104 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 105 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13)); 106 | } 107 | 108 | @Override 109 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 110 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14)); 111 | } 112 | 113 | @Override 114 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 115 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15)); 116 | } 117 | 118 | @Override 119 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 120 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16)); 121 | } 122 | 123 | @Override 124 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 125 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17)); 126 | } 127 | 128 | @Override 129 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 130 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18)); 131 | } 132 | 133 | @Override 134 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 135 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19)); 136 | } 137 | 138 | @Override 139 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 140 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20)); 141 | } 142 | 143 | @Override 144 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 145 | Object[] allArgs = new Object[20 + args.length]; 146 | System.arraycopy(args, 0, allArgs, 20, args.length); 147 | allArgs[0] = arg1; 148 | allArgs[1] = arg2; 149 | allArgs[2] = arg3; 150 | allArgs[3] = arg4; 151 | allArgs[4] = arg5; 152 | allArgs[5] = arg6; 153 | allArgs[6] = arg7; 154 | allArgs[7] = arg8; 155 | allArgs[8] = arg9; 156 | allArgs[9] = arg10; 157 | allArgs[10] = arg11; 158 | allArgs[11] = arg12; 159 | allArgs[12] = arg13; 160 | allArgs[13] = arg14; 161 | allArgs[14] = arg15; 162 | allArgs[15] = arg16; 163 | allArgs[16] = arg17; 164 | allArgs[17] = arg18; 165 | allArgs[18] = arg19; 166 | allArgs[19] = arg20; 167 | return upstream.cached(segment, ArraySeq.create(allArgs)); 168 | } 169 | 170 | @Override 171 | public Object applyTo(ISeq arglist) { 172 | return upstream.cached(segment, arglist); 173 | } 174 | 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /java/memento/mount/CachedFn.java: -------------------------------------------------------------------------------- 1 | package memento.mount; 2 | 3 | import clojure.lang.*; 4 | import memento.base.ICache; 5 | import memento.base.Segment; 6 | 7 | public class CachedFn extends AFunction implements IMountPoint, Cached { 8 | private final Object reloadGuard; 9 | private final IFn originalFn; 10 | private final Segment segment; 11 | 12 | @Override 13 | public IPersistentMap asMap() { 14 | return mp.asMap(); 15 | } 16 | 17 | @Override 18 | public Object cached(ISeq args) { 19 | return mp.cached(args); 20 | } 21 | 22 | @Override 23 | public Object ifCached(ISeq args) { 24 | return mp.ifCached(args); 25 | } 26 | 27 | @Override 28 | public Object getTags() { 29 | return mp.getTags(); 30 | } 31 | 32 | @Override 33 | public Object handleEvent(Object event) { 34 | return mp.handleEvent(event); 35 | } 36 | 37 | @Override 38 | public ICache invalidate(ISeq args) { 39 | return mp.invalidate(args); 40 | } 41 | 42 | @Override 43 | public ICache invalidateAll() { 44 | return mp.invalidateAll(); 45 | } 46 | 47 | @Override 48 | public ICache mountedCache() { 49 | return mp.mountedCache(); 50 | } 51 | 52 | @Override 53 | public Segment segment() { 54 | return mp.segment(); 55 | } 56 | 57 | @Override 58 | public ICache addEntries(IPersistentMap argsToVals) { 59 | return mp.addEntries(argsToVals); 60 | } 61 | 62 | private final IMountPoint mp; 63 | private final IPersistentMap meta; 64 | 65 | public CachedFn(Object reloadGuard, IMountPoint mp, IPersistentMap meta, IFn originalFn) { 66 | this.reloadGuard = reloadGuard; 67 | this.mp = mp; 68 | this.meta = meta; 69 | this.originalFn = originalFn; 70 | this.segment = mp.segment(); 71 | } 72 | 73 | @Override 74 | public IPersistentMap meta() { 75 | return meta; 76 | } 77 | 78 | @Override 79 | public IObj withMeta(IPersistentMap meta) { 80 | return new CachedFn(reloadGuard, mp, meta, originalFn); 81 | } 82 | 83 | @Override 84 | public Object call() { 85 | return mp.mountedCache().cached(segment, ArraySeq.create()); 86 | } 87 | 88 | @Override 89 | public void run() { 90 | mp.mountedCache().cached(segment, ArraySeq.create()); 91 | } 92 | 93 | @Override 94 | public Object invoke() { 95 | return mp.mountedCache().cached(segment, ArraySeq.create()); 96 | } 97 | 98 | @Override 99 | public Object invoke(Object arg1) { 100 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1)); 101 | } 102 | 103 | @Override 104 | public Object invoke(Object arg1, Object arg2) { 105 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2)); 106 | } 107 | 108 | @Override 109 | public Object invoke(Object arg1, Object arg2, Object arg3) { 110 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3)); 111 | } 112 | 113 | @Override 114 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 115 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4)); 116 | } 117 | 118 | @Override 119 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 120 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5)); 121 | } 122 | 123 | @Override 124 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 125 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6)); 126 | } 127 | 128 | @Override 129 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 130 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7)); 131 | } 132 | 133 | @Override 134 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 135 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)); 136 | } 137 | 138 | @Override 139 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 140 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)); 141 | } 142 | 143 | @Override 144 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 145 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); 146 | } 147 | 148 | @Override 149 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 150 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11)); 151 | } 152 | 153 | @Override 154 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 155 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12)); 156 | } 157 | 158 | @Override 159 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 160 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13)); 161 | } 162 | 163 | @Override 164 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 165 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14)); 166 | } 167 | 168 | @Override 169 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 170 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15)); 171 | } 172 | 173 | @Override 174 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 175 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16)); 176 | } 177 | 178 | @Override 179 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 180 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17)); 181 | } 182 | 183 | @Override 184 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 185 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18)); 186 | } 187 | 188 | @Override 189 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 190 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19)); 191 | } 192 | 193 | @Override 194 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 195 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20)); 196 | } 197 | 198 | @Override 199 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 200 | Object[] allArgs = new Object[20 + args.length]; 201 | System.arraycopy(args, 0, allArgs, 20, args.length); 202 | allArgs[0] = arg1; 203 | allArgs[1] = arg2; 204 | allArgs[2] = arg3; 205 | allArgs[3] = arg4; 206 | allArgs[4] = arg5; 207 | allArgs[5] = arg6; 208 | allArgs[6] = arg7; 209 | allArgs[7] = arg8; 210 | allArgs[8] = arg9; 211 | allArgs[9] = arg10; 212 | allArgs[10] = arg11; 213 | allArgs[11] = arg12; 214 | allArgs[12] = arg13; 215 | allArgs[13] = arg14; 216 | allArgs[14] = arg15; 217 | allArgs[15] = arg16; 218 | allArgs[16] = arg17; 219 | allArgs[17] = arg18; 220 | allArgs[18] = arg19; 221 | allArgs[19] = arg20; 222 | return mp.mountedCache().cached(segment, ArraySeq.create(allArgs)); 223 | } 224 | 225 | @Override 226 | public Object applyTo(ISeq arglist) { 227 | return mp.mountedCache().cached(segment, arglist); 228 | } 229 | 230 | public IMountPoint getMp() { 231 | return mp; 232 | } 233 | 234 | public IFn getOriginalFn() { 235 | return originalFn; 236 | } 237 | 238 | @Override 239 | public String toString() { 240 | return "CachedFn{" + 241 | "originalFn=" + originalFn + 242 | ", segment=" + segment + 243 | ", mp=" + mp + 244 | ", meta=" + meta + 245 | '}'; 246 | } 247 | 248 | public Segment getSegment() { 249 | return segment; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/memento/core.clj: -------------------------------------------------------------------------------- 1 | (ns memento.core 2 | "Memoization library." 3 | {:author "Rok Lenarčič"} 4 | (:require [memento.base :as base] 5 | [memento.caffeine] 6 | [memento.multi :as multi] 7 | [memento.mount :as mount]) 8 | (:import (java.util IdentityHashMap) 9 | (java.util.function BiFunction) 10 | (memento.base EntryMeta ICache LockoutTag) 11 | (memento.mount Cached IMountPoint))) 12 | 13 | (defn do-not-cache 14 | "Wrap a function result value in a wrapper that tells the Cache not to 15 | cache this particular value." 16 | [v] 17 | (if (instance? EntryMeta v) 18 | (do (.setNoCache ^EntryMeta v true) v) 19 | (EntryMeta. v true #{}))) 20 | 21 | (defn with-tag-id 22 | "Wrap a function result value in a wrapper that has the given additional 23 | tag + ID information. You can add multiple IDs for same tag. 24 | 25 | This information is later used by memo-clear-tag!." 26 | [v tag id] 27 | (if (instance? EntryMeta v) 28 | (do (.setTagIdents ^EntryMeta v (conj (.getTagIdents ^EntryMeta v) [tag id])) v) 29 | (EntryMeta. v false #{[tag id]}))) 30 | 31 | (defn create 32 | "Create a cache. 33 | 34 | A conf is a map of cache settings, see memento.config namespace for names of settings." 35 | [conf] 36 | (base/base-create-cache conf)) 37 | 38 | (defn bind 39 | "Bind the cache to a function or a var. If a var is specified, then var root 40 | binding is modified. 41 | 42 | The mount-conf is a configuration options for mount point. 43 | 44 | It can be a map with options, a vector of tags, or one tag. 45 | 46 | Supported options are: 47 | - memento.core/key-fn 48 | - memento.core/key-fn* 49 | - memento.core/ret-fn 50 | - memento.core/tags 51 | - memento.core/seed" 52 | [fn-or-var mount-conf cache] 53 | (when-not (instance? ICache cache) 54 | (throw (IllegalArgumentException. "Argument should satisfy memento.base/Cache"))) 55 | (mount/bind fn-or-var mount-conf cache)) 56 | 57 | (defn memo 58 | "Combines cache create and bind operations from this namespace. 59 | 60 | If conf is provided, it is used as mount-conf in bind operation, but with any extra map keys 61 | going into cache create configuration. 62 | 63 | If no configuration is provided, meta of the fn or var is examined. 64 | 65 | The value of :memento.core/cache meta key is used as conf parameter 66 | in memento.core/memo. If :memento.core/mount key is also present, then 67 | they are used as cache and conf parameters respectively." 68 | ([fn-or-var] 69 | (let [{::keys [mount cache]} (meta fn-or-var)] 70 | (if mount (memo fn-or-var mount cache) 71 | (memo fn-or-var cache)))) 72 | ([fn-or-var conf] 73 | (if (map? conf) 74 | (memo fn-or-var 75 | (select-keys conf mount/configuration-props) 76 | (apply dissoc conf mount/configuration-props)) 77 | (memo fn-or-var conf {}))) 78 | ([fn-or-var mount-conf cache-conf] 79 | (->> cache-conf 80 | create 81 | (bind fn-or-var mount-conf)))) 82 | 83 | (defmacro defmemo 84 | "Like defn, but immediately wraps var in a memo call. It expects caching configuration 85 | to be in meta under memento.core/cache key, as expected by memo." 86 | {:arglists '([name doc-string? attr-map? [params*] prepost-map? body] 87 | [name doc-string? attr-map? ([params*] prepost-map? body)+ attr-map?])} 88 | [& body] 89 | `(memo (defn ~@body))) 90 | 91 | (defn active-cache 92 | "Return Cache instance from the function, if present." 93 | [f] (some-> (mount/mount-point f) mount/mounted-cache)) 94 | 95 | (defn memoized? 96 | "Returns true if function is memoized." 97 | [f] (instance? Cached f)) 98 | 99 | (defn memo-unwrap 100 | "Takes a function and returns an uncached function." 101 | [f] (if (instance? Cached f) (.getOriginalFn ^Cached f) f)) 102 | 103 | (defn memo-clear-cache! 104 | "Invalidate all entries in Cache. Returns cache." 105 | [cache] 106 | (when-not (instance? ICache cache) 107 | (throw (IllegalArgumentException. "Argument should satisfy memento.base/Cache"))) 108 | (base/invalidate-all cache)) 109 | 110 | (defn none-cache? 111 | "Returns true if this cache is one that does no caching." 112 | [cache] 113 | (= cache base/no-cache)) 114 | 115 | (defn memo-clear! 116 | "Invalidate one entry (f with arglist) on memoized function f, 117 | or invalidate all entries for memoized function. Returns f." 118 | ([f] 119 | (when-let [^IMountPoint mp (mount/mount-point f)] (.invalidateAll mp)) 120 | f) 121 | ([f & fargs] 122 | (when-let [^IMountPoint mp (mount/mount-point f)] (.invalidate mp fargs)) 123 | f)) 124 | 125 | (defn memo-add! 126 | "Add map's entries to the cache. The keys are argument-lists. 127 | 128 | Returns f." 129 | [f m] 130 | (when-let [^IMountPoint mp (mount/mount-point f)] (.addEntries mp m)) 131 | f) 132 | 133 | (defn as-map 134 | "Return a map representation of the memoized entries on this function." 135 | [f] 136 | (when-let [^IMountPoint mp (mount/mount-point f)] (.asMap mp))) 137 | 138 | (defn tags 139 | "Return tags of the memoized function." 140 | [f] 141 | (when-let [^IMountPoint mp (mount/mount-point f)] (.getTags mp))) 142 | 143 | (defn mounts-by-tag 144 | "Returns a sequence of MountPoint instances used by memoized functions which are tagged by this tag." 145 | [tag] 146 | (get @mount/tags tag [])) 147 | 148 | (defn caches-by-tag 149 | "Returns a collection of distinct caches that are mounted with a tag" 150 | [tag] 151 | (let [m (IdentityHashMap.)] 152 | (run! #(.put m (mount/mounted-cache %) nil) (mounts-by-tag tag)) 153 | (.keySet m))) 154 | 155 | (defn fire-event! 156 | "Fire an event payload to the single cached function or all tagged functions, if tag 157 | is provided." 158 | [f-or-tag evt] 159 | (if (instance? IMountPoint f-or-tag) 160 | (.handleEvent ^IMountPoint f-or-tag evt) 161 | (->> (mounts-by-tag f-or-tag) 162 | (eduction (map #(.handleEvent ^IMountPoint % evt))) 163 | dorun))) 164 | 165 | (defn memo-clear-tags! 166 | "Invalidate all entries that have the specified tag + id metadata. ID can be anything. 167 | 168 | Expects a collection of [tag id] pairs." 169 | [& tag+ids] 170 | (let [cache->ids (IdentityHashMap.) 171 | _ (doseq [[tag tag+ids] (group-by first tag+ids) 172 | cache (caches-by-tag tag)] 173 | (.compute 174 | cache->ids 175 | cache 176 | (reify BiFunction 177 | (apply [this k v] (into (or v []) tag+ids))))) 178 | tag (LockoutTag.)] 179 | (try 180 | (.startLockout base/lockout-map tag+ids tag) 181 | (run! (fn [e] (base/invalidate-ids (key e) (val e))) cache->ids) 182 | (finally 183 | (.endLockout base/lockout-map tag+ids tag))))) 184 | 185 | (defn memo-clear-tag! 186 | "Invalidate all entries that have the specified tag + id metadata. ID can be anything." 187 | [tag id] 188 | (memo-clear-tags! [tag id])) 189 | 190 | (defn update-tag-caches! 191 | "For each memoized function with the specified tag, set the Cache used by the fn to (cache-fn current-cache). 192 | 193 | Cache update function is ran on each 194 | memoized function (mount point), so if one cache is backing multiple functions, the cache update function is called 195 | multiple times on it. If you want to run cache-fn one each Cache instance only once, I recommend wrapping it 196 | in clojure.core/memoize. 197 | 198 | If caches are thread-bound to a different value with with-caches, then those 199 | bindings are modified instead of root bindings." 200 | [tag cache-fn] 201 | (mount/alter-caches-mapping tag mount/update-existing cache-fn)) 202 | 203 | (defmacro with-caches 204 | "Within the block, each memoized function with the specified tag has its cache update by cache-fn. 205 | 206 | The values are bound within the block as a thread local binding. Cache update function is ran on each 207 | memoized function (mount point), so if one cache is backing multiple functions, the cache update function is called 208 | multiple timed on it. If you want to run cache-fn one each Cache instance only once, I recommend wrapping it 209 | in clojure.core/memoize." 210 | [tag cache-fn & body] 211 | `(binding [mount/*caches* (mount/update-existing mount/*caches* (get @mount/tags ~tag []) ~cache-fn)] 212 | ~@body)) 213 | 214 | (defn evt-cache-add 215 | "Convenience function. It creates or wraps event handler fn, 216 | with an implementation which expects an event to be a vector of 217 | [event-type payload], it checks for matching event type and inserts 218 | the result of (->entries payload) into the cache." 219 | ([evt-type ->entries] (evt-cache-add (constantly nil) evt-type ->entries)) 220 | ([evt-fn evt-type ->entries] 221 | (fn [mountp evt] 222 | (when (and (vector? evt) 223 | (= (count evt) 2) 224 | (= evt-type (first evt))) 225 | (memo-add! mountp (->entries (second evt)))) 226 | (evt-fn mountp evt)))) 227 | 228 | (defn tiered 229 | "Creates a configuration for a tiered cache. Both parameters are either a conf map or a cache. 230 | 231 | Entry is fetched from cache, delegating to upstream is not found. After the operation 232 | the entry is in both caches. 233 | 234 | Useful when upstream is a big cache that outside the JVM, but it's not that inexpensive, so you 235 | want a local smaller cache in front of it. 236 | 237 | Invalidation operations also affect upstream. Other operations only affect local cache." 238 | [cache upstream] 239 | {::type ::tiered 240 | ::multi/cache cache 241 | ::multi/upstream upstream}) 242 | 243 | (defn consulting 244 | "Creates a configuration for a consulting tiered cache. Both parameters are either a conf map or a cache. 245 | 246 | Entry is fetched from cache, if not found, the upstream is asked for entry if present (but not to make one 247 | in the upstream). 248 | 249 | After the operation, the entry is in local cache, upstream is unchanged. 250 | 251 | Useful when you want to consult a long term upstream cache for existing entries, but you don't want any 252 | entries being created for the short term cache to be pushed upstream. 253 | 254 | Invalidation operations also affect upstream. Other operations only affect local cache." 255 | [cache upstream] 256 | {::type ::consulting 257 | ::multi/cache cache 258 | ::multi/upstream upstream}) 259 | 260 | (defn daisy 261 | "Creates a configuration for a daisy chained cache. Cache parameter is a conf map or a cache. 262 | 263 | Entry is returned from cache IF PRESENT, otherwise upstream is hit. The returned value 264 | is NOT added to cache. 265 | 266 | After the operation the entry is either in local or upstream cache. 267 | 268 | Useful when you don't want entries from upstream accumulating in local 269 | cache, and you're feeding the local cache via some other means: 270 | - a preloaded fixed cache 271 | - manually adding entries 272 | 273 | Invalidation operations also affect upstream. Other operations only affect local cache." 274 | [cache upstream] 275 | {::type ::daisy 276 | ::multi/cache cache 277 | ::multi/upstream upstream}) 278 | 279 | (defmacro if-cached 280 | "Like if-let, but then clause is executed if the call in the binding is cached, with the binding symbol 281 | being bound to the cached value. 282 | 283 | This assumes that the top form in bindings is a call of cached function, generating an error otherwise. 284 | 285 | e.g. (if-cached [my-val (my-cached-fn arg1)] ...)" 286 | ([bindings then] 287 | `(if-cached ~bindings ~then nil)) 288 | ([bindings then else] 289 | (assert (vector? bindings)) 290 | (assert (= 2 (count bindings))) 291 | (let [form (bindings 0) 292 | cache-call (bindings 1) 293 | _ (assert (list? cache-call)) 294 | f (first cache-call) 295 | _ (assert (symbol? f))] 296 | `(if-let [mnt# (mount/mount-point ~(first cache-call))] 297 | (let [mnt# (if (instance? Cached mnt#) (.getMp mnt#) mnt#) 298 | cached# (.ifCached mnt# '~(next cache-call))] 299 | (if (= cached# base/absent) 300 | ~else 301 | (let [~form cached#] ~then))) 302 | (throw (ex-info (str "Function " ~(str f) " is not a cached function") 303 | {:form '~cache-call})))))) 304 | -------------------------------------------------------------------------------- /java/memento/multi/ConsultingCache.java: -------------------------------------------------------------------------------- 1 | package memento.multi; 2 | 3 | import clojure.lang.*; 4 | import memento.base.ICache; 5 | import memento.base.Segment; 6 | 7 | public class ConsultingCache extends MultiCache { 8 | public ConsultingCache(ICache cache, ICache upstream, IPersistentMap conf, Object absent) { 9 | super(cache, upstream, conf, absent); 10 | } 11 | 12 | @Override 13 | public Object cached(Segment segment, ISeq args) { 14 | return cache.cached(segment.withFn(new UpstreamOrCalc(segment)), args); 15 | } 16 | 17 | private class UpstreamOrCalc implements IFn { 18 | 19 | private Segment segment; 20 | 21 | public UpstreamOrCalc(Segment segment) { 22 | this.segment = segment; 23 | } 24 | 25 | @Override 26 | public Object call() { 27 | ISeq s = ArraySeq.create(); 28 | Object up = upstream.ifCached(segment, s); 29 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 30 | } 31 | 32 | @Override 33 | public void run() { 34 | ISeq s = ArraySeq.create(); 35 | Object up = upstream.ifCached(segment, s); 36 | if (up == absent) { 37 | AFn.applyToHelper(segment.getF(), s); 38 | } 39 | } 40 | 41 | @Override 42 | public Object invoke() { 43 | ISeq s = ArraySeq.create(); 44 | Object up = upstream.ifCached(segment, s); 45 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 46 | } 47 | 48 | @Override 49 | public Object invoke(Object arg1) { 50 | ISeq s = ArraySeq.create(arg1); 51 | Object up = upstream.ifCached(segment, s); 52 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 53 | } 54 | 55 | @Override 56 | public Object invoke(Object arg1, Object arg2) { 57 | ISeq s = ArraySeq.create(arg1, arg2); 58 | Object up = upstream.ifCached(segment, s); 59 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 60 | } 61 | 62 | @Override 63 | public Object invoke(Object arg1, Object arg2, Object arg3) { 64 | ISeq s = ArraySeq.create(arg1, arg2, arg3); 65 | Object up = upstream.ifCached(segment, s); 66 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 67 | } 68 | 69 | @Override 70 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 71 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4); 72 | Object up = upstream.ifCached(segment, s); 73 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 74 | } 75 | 76 | @Override 77 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 78 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5); 79 | Object up = upstream.ifCached(segment, s); 80 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 81 | } 82 | 83 | @Override 84 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 85 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6); 86 | Object up = upstream.ifCached(segment, s); 87 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 88 | } 89 | 90 | @Override 91 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 92 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7); 93 | Object up = upstream.ifCached(segment, s); 94 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 95 | } 96 | 97 | @Override 98 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 99 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); 100 | Object up = upstream.ifCached(segment, s); 101 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 102 | } 103 | 104 | @Override 105 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 106 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); 107 | Object up = upstream.ifCached(segment, s); 108 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 109 | } 110 | 111 | @Override 112 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 113 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10); 114 | Object up = upstream.ifCached(segment, s); 115 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 116 | } 117 | 118 | @Override 119 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 120 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11); 121 | Object up = upstream.ifCached(segment, s); 122 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 123 | } 124 | 125 | @Override 126 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 127 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12); 128 | Object up = upstream.ifCached(segment, s); 129 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 130 | } 131 | 132 | @Override 133 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 134 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13); 135 | Object up = upstream.ifCached(segment, s); 136 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 137 | } 138 | 139 | @Override 140 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 141 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14); 142 | Object up = upstream.ifCached(segment, s); 143 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 144 | } 145 | 146 | @Override 147 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 148 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15); 149 | Object up = upstream.ifCached(segment, s); 150 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 151 | } 152 | 153 | @Override 154 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 155 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16); 156 | Object up = upstream.ifCached(segment, s); 157 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 158 | } 159 | 160 | @Override 161 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 162 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17); 163 | Object up = upstream.ifCached(segment, s); 164 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 165 | } 166 | 167 | @Override 168 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 169 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18); 170 | Object up = upstream.ifCached(segment, s); 171 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 172 | } 173 | 174 | @Override 175 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 176 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19); 177 | Object up = upstream.ifCached(segment, s); 178 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 179 | } 180 | 181 | @Override 182 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 183 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20); 184 | Object up = upstream.ifCached(segment, s); 185 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 186 | } 187 | 188 | @Override 189 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 190 | Object[] allArgs = new Object[20 + args.length]; 191 | System.arraycopy(args, 0, allArgs, 20, args.length); 192 | allArgs[0] = arg1; 193 | allArgs[1] = arg2; 194 | allArgs[2] = arg3; 195 | allArgs[3] = arg4; 196 | allArgs[4] = arg5; 197 | allArgs[5] = arg6; 198 | allArgs[6] = arg7; 199 | allArgs[7] = arg8; 200 | allArgs[8] = arg9; 201 | allArgs[9] = arg10; 202 | allArgs[10] = arg11; 203 | allArgs[11] = arg12; 204 | allArgs[12] = arg13; 205 | allArgs[13] = arg14; 206 | allArgs[14] = arg15; 207 | allArgs[15] = arg16; 208 | allArgs[16] = arg17; 209 | allArgs[17] = arg18; 210 | allArgs[18] = arg19; 211 | allArgs[19] = arg20; 212 | ISeq s = ArraySeq.create(allArgs); 213 | Object up = upstream.ifCached(segment, s); 214 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 215 | } 216 | 217 | @Override 218 | public Object applyTo(ISeq arglist) { 219 | Object up = upstream.ifCached(segment, arglist); 220 | return up == absent ? AFn.applyToHelper(segment.getF(), arglist) : up; 221 | } 222 | 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /java/memento/mount/CachedMultiFn.java: -------------------------------------------------------------------------------- 1 | package memento.mount; 2 | 3 | import clojure.lang.*; 4 | import memento.base.ICache; 5 | import memento.base.Segment; 6 | 7 | public class CachedMultiFn extends MultiFn implements IMountPoint, Cached, IObj { 8 | private final Object reloadGuard; 9 | private final MultiFn originalFn; 10 | private final Segment segment; 11 | 12 | @Override 13 | public IPersistentMap asMap() { 14 | return mp.asMap(); 15 | } 16 | 17 | @Override 18 | public Object cached(ISeq args) { 19 | return mp.cached(args); 20 | } 21 | 22 | @Override 23 | public Object ifCached(ISeq args) { 24 | return mp.ifCached(args); 25 | } 26 | 27 | @Override 28 | public Object getTags() { 29 | return mp.getTags(); 30 | } 31 | 32 | @Override 33 | public Object handleEvent(Object event) { 34 | return mp.handleEvent(event); 35 | } 36 | 37 | @Override 38 | public ICache invalidate(ISeq args) { 39 | return mp.invalidate(args); 40 | } 41 | 42 | @Override 43 | public ICache invalidateAll() { 44 | return mp.invalidateAll(); 45 | } 46 | 47 | @Override 48 | public ICache mountedCache() { 49 | return mp.mountedCache(); 50 | } 51 | 52 | @Override 53 | public Segment segment() { 54 | return mp.segment(); 55 | } 56 | 57 | @Override 58 | public ICache addEntries(IPersistentMap argsToVals) { 59 | return mp.addEntries(argsToVals); 60 | } 61 | 62 | private final String name; 63 | private final IMountPoint mp; 64 | private final IPersistentMap meta; 65 | 66 | public CachedMultiFn(String name, Object reloadGuard, IMountPoint mp, IPersistentMap meta, MultiFn originalFn) { 67 | super(name, originalFn.dispatchFn, originalFn.defaultDispatchVal, originalFn.hierarchy); 68 | this.reloadGuard = reloadGuard; 69 | this.mp = mp; 70 | this.name = name; 71 | this.meta = meta; 72 | this.originalFn = originalFn; 73 | this.segment = mp.segment(); 74 | } 75 | 76 | @Override 77 | public IPersistentMap meta() { 78 | return meta; 79 | } 80 | 81 | @Override 82 | public IObj withMeta(IPersistentMap meta) { 83 | return new CachedMultiFn(name, reloadGuard, mp, meta, originalFn); 84 | } 85 | 86 | @Override 87 | public Object call() { 88 | return mp.mountedCache().cached(segment, ArraySeq.create()); 89 | } 90 | 91 | @Override 92 | public void run() { 93 | mp.mountedCache().cached(segment, ArraySeq.create()); 94 | } 95 | 96 | @Override 97 | public Object invoke() { 98 | return mp.mountedCache().cached(segment, ArraySeq.create()); 99 | } 100 | 101 | @Override 102 | public Object invoke(Object arg1) { 103 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1)); 104 | } 105 | 106 | @Override 107 | public Object invoke(Object arg1, Object arg2) { 108 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2)); 109 | } 110 | 111 | @Override 112 | public Object invoke(Object arg1, Object arg2, Object arg3) { 113 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3)); 114 | } 115 | 116 | @Override 117 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 118 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4)); 119 | } 120 | 121 | @Override 122 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 123 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5)); 124 | } 125 | 126 | @Override 127 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 128 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6)); 129 | } 130 | 131 | @Override 132 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 133 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7)); 134 | } 135 | 136 | @Override 137 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 138 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)); 139 | } 140 | 141 | @Override 142 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 143 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)); 144 | } 145 | 146 | @Override 147 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 148 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); 149 | } 150 | 151 | @Override 152 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 153 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11)); 154 | } 155 | 156 | @Override 157 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 158 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12)); 159 | } 160 | 161 | @Override 162 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 163 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13)); 164 | } 165 | 166 | @Override 167 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 168 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14)); 169 | } 170 | 171 | @Override 172 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 173 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15)); 174 | } 175 | 176 | @Override 177 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 178 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16)); 179 | } 180 | 181 | @Override 182 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 183 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17)); 184 | } 185 | 186 | @Override 187 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 188 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18)); 189 | } 190 | 191 | @Override 192 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 193 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19)); 194 | } 195 | 196 | @Override 197 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 198 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20)); 199 | } 200 | 201 | @Override 202 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 203 | Object[] allArgs = new Object[20 + args.length]; 204 | System.arraycopy(args, 0, allArgs, 20, args.length); 205 | allArgs[0] = arg1; 206 | allArgs[1] = arg2; 207 | allArgs[2] = arg3; 208 | allArgs[3] = arg4; 209 | allArgs[4] = arg5; 210 | allArgs[5] = arg6; 211 | allArgs[6] = arg7; 212 | allArgs[7] = arg8; 213 | allArgs[8] = arg9; 214 | allArgs[9] = arg10; 215 | allArgs[10] = arg11; 216 | allArgs[11] = arg12; 217 | allArgs[12] = arg13; 218 | allArgs[13] = arg14; 219 | allArgs[14] = arg15; 220 | allArgs[15] = arg16; 221 | allArgs[16] = arg17; 222 | allArgs[17] = arg18; 223 | allArgs[18] = arg19; 224 | allArgs[19] = arg20; 225 | return mp.mountedCache().cached(segment, ArraySeq.create(allArgs)); 226 | } 227 | 228 | @Override 229 | public Object applyTo(ISeq arglist) { 230 | return mp.mountedCache().cached(segment, arglist); 231 | } 232 | 233 | public IMountPoint getMp() { 234 | return mp; 235 | } 236 | 237 | public IFn getOriginalFn() { 238 | return originalFn; 239 | } 240 | 241 | @Override 242 | public String toString() { 243 | return "CachedMultiFn{" + 244 | "originalFn=" + originalFn + 245 | ", segment=" + segment + 246 | ", mp=" + mp + 247 | ", meta=" + meta + 248 | '}'; 249 | } 250 | 251 | public Segment getSegment() { 252 | return segment; 253 | } 254 | 255 | @Override 256 | public MultiFn addMethod(Object dispatchVal, IFn method) { 257 | originalFn.addMethod(dispatchVal, method); 258 | return this; 259 | } 260 | 261 | @Override 262 | public IFn getMethod(Object dispatchVal) { 263 | return originalFn.getMethod(dispatchVal); 264 | } 265 | 266 | @Override 267 | public IPersistentMap getMethodTable() { 268 | return originalFn == null ? PersistentHashMap.EMPTY : originalFn.getMethodTable(); 269 | } 270 | 271 | @Override 272 | public IPersistentMap getPreferTable() { 273 | return originalFn.getPreferTable(); 274 | } 275 | 276 | @Override 277 | public MultiFn preferMethod(Object dispatchValX, Object dispatchValY) { 278 | originalFn.preferMethod(dispatchValX, dispatchValY); 279 | return this; 280 | } 281 | 282 | @Override 283 | public MultiFn removeMethod(Object dispatchVal) { 284 | originalFn.removeMethod(dispatchVal); 285 | return this; 286 | } 287 | 288 | @Override 289 | public MultiFn reset() { 290 | originalFn.reset(); 291 | return this; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Memento 2 | 3 | A library for function memoization with scoped caches and tagged eviction capabilities. 4 | 5 | ## Dependency 6 | 7 | [](https://clojars.org/org.clojars.roklenarcic/memento) 8 | 9 | ## Version 2.0 breaking changes 10 | 11 | Version 2 moves from Java 8 to Java 11 as minimum JVM version. Caffeine version is 3 instead of 2. 12 | 13 | ## Version 1.0 breaking changes 14 | 15 | Version 1.0 represents a switch from Guava to Caffeine, which is a faster caching library, with added 16 | benefit of not pulling in the whole Guava artefact which is more that just that Cache. The Guava Cache type 17 | key and the config namespace are deprecated and will be removed in the future. 18 | 19 | ## Motivation 20 | 21 | Why is there a need for another caching library? 22 | 23 | - request scoped caching (and other scoped caching) 24 | - eviction by secondary index 25 | - disabling cache for specific function returns 26 | - tiered caching 27 | - size based eviction that puts limits around more than one function at the time 28 | - cache events 29 | 30 | ## Performance 31 | 32 | - [**Performance**](doc/performance.md) 33 | 34 | ## Adding cache to a function 35 | 36 | **With require `[memento.core :as m][memento.config :as mc]`:** 37 | 38 | Define a function + create new cache + attach cache to a function: 39 | 40 | ```clojure 41 | (m/defmemo my-function 42 | {::m/cache {mc/type mc/caffeine}} 43 | [x] 44 | (* 2 x)) 45 | ``` 46 | 47 | ### **The key parts here**: 48 | - `defmemo` works just like `defn` but wraps the function in a cache 49 | - specify the cache configuration via `:memento.core/cache` keyword in function meta 50 | 51 | Quick reminder, there are two ways to provide metadata when defining functions: `defn` allows a meta 52 | map to be provided before the argument list, or you can add meta to the symbol directly as supported by the reader: 53 | 54 | ```clojure 55 | (m/defmemo ^{::m/cache {mc/type mc/caffeine}} my-function 56 | [x] 57 | (* 2 x)) 58 | ``` 59 | 60 | ### Caching an anonymous function 61 | 62 | You can add cache to a function object (in `clojure.core/memoize` fashion): 63 | 64 | ```clojure 65 | (m/memo (fn [] ...) {mc/type mc/caffeine}) 66 | ``` 67 | 68 | ### Other ways to attach Cache to a function 69 | 70 | [Caches and memoize calls](doc/major.md) 71 | 72 | ## Cache conf(iguration) 73 | 74 | See above: `{mc/type mc/caffeine}` 75 | 76 | The cache conf is an open map of namespaced keywords such as `:memento.core/type`, various cache implementations can 77 | use implementation specific config keywords. 78 | 79 | Learning all the keywords and what they do can be hard. To assist you 80 | there are special conf namespaces provided where conf keywords are defined as vars with docs, 81 | so it's easy so you to see which configuration keys are available and what their function is. It also helps 82 | prevent bugs from typing errors. 83 | 84 | The core properties are defined in `[memento.config :as mc]` namespace. Caffeine specific properties are defined 85 | in `[memento.caffeine.config :as mcc]`. 86 | 87 | Here's a couple of equal ways of writing out you cache configuration meta: 88 | 89 | ```clojure 90 | ; the longest 91 | {:memento.core/cache {:memento.core/type :memento.core/caffeine}} 92 | ; using alias 93 | {::m/cache {::m/type ::m/caffeine}} 94 | ; using memento.config vars - recommended 95 | {mc/cache {mc/type mc/caffeine}} 96 | ``` 97 | 98 | ### Core conf 99 | 100 | The core configuration properties: 101 | 102 | #### mc/type 103 | 104 | Cache implementation type, e.g. caffeine, redis, see the implementation library docs. **Make sure 105 | you load the implementation namespace at some point!**. Caffeine namespace is loaded automatically 106 | when memento.core is loaded. 107 | 108 | #### mc/size< 109 | 110 | Size limit expressed in number of entries or total weight if implementation supports weighted cache entries 111 | 112 | #### mc/ttl 113 | 114 | Entry is invalid after this amount of time has passed since its creation 115 | 116 | It's either a number (of seconds), a pair describing duration e.g. `[10 :m]` for 10 minutes, 117 | see `memento.config/timeunits` for timeunits. 118 | 119 | #### mc/fade 120 | 121 | Entry is invalid after this amount of time has passed since last access, see `mc/ttl` for duration 122 | specification. 123 | 124 | #### mc/key-fn, mc/key-fn* 125 | 126 | Specify a function that will transform the function arg list into the final cache key. Used 127 | to drop function arguments that shouldn't factor into cache tag equality. 128 | 129 | The `key-fn` receives a sequence of arguments, `key-fn*` receives multiple arguments as if it 130 | was the function itself. 131 | 132 | See: [Changing the key for cached tag](doc/key-fn.md) 133 | 134 | #### mc/ret-fn 135 | 136 | A function that is called on every cached function return value. Used for general transformations 137 | of return values. 138 | 139 | #### mc/ret-ex-fn 140 | 141 | A function that is called on every thrown Throwable. Used for general transformations 142 | of thrown exceptions values. 143 | 144 | #### mc/seed 145 | 146 | Initial entries to load in the cache. 147 | 148 | #### mc/initial-capacity 149 | 150 | Cache capacity hint to implementation. 151 | 152 | ## Conf is a value (map) 153 | 154 | Cache conf can get quite involved: 155 | 156 | ```clojure 157 | (ns memento.tryout 158 | (:require [memento.core :as m] 159 | ; general cache conf keys 160 | [memento.config :as mc] 161 | ; caffeine specific cache conf keys 162 | [memento.caffeine.config :as mcc])) 163 | 164 | (def my-weird-cache 165 | "Conf for caffeine cache that caches up to 20 seconds and up to 30 entries, uses weak 166 | references and prints when keys get evicted." 167 | {mc/type mc/caffeine 168 | mc/size< 30 169 | mc/ttl 20 170 | mcc/weak-values true 171 | mcc/removal-listener #(println (apply format "Function %s key %s, value %s got evicted because of %s" %&))}) 172 | 173 | (m/defmemo my-function 174 | {::m/cache my-weird-cache} 175 | [x] (* 2 x)) 176 | ``` 177 | 178 | Seeing as cache conf is a map, I recommend a pattern where you have a namespace in your application that contains vars 179 | with your commonly used cache conf maps and functions that generate slightly parameterized 180 | configuration. E.g. 181 | 182 | ```clojure 183 | (ns my-project.cache 184 | (:require [memento.config :as mc])) 185 | 186 | ;; infinite cache 187 | (def inf-cache {mc/type mc/caffeine}) 188 | 189 | (defn for-seconds [n] (assoc inf-cache mc/ttl n)) 190 | ``` 191 | 192 | Then you just use that in your code: 193 | 194 | ```clojure 195 | (m/defmemo my-function 196 | {::m/cache (cache/for-seconds 60)} 197 | [x] (* x 2)) 198 | ``` 199 | 200 | ## Caches and mount points 201 | 202 | Enabling memoization of a function is composed of two distinct steps: 203 | 204 | - creating a Cache (optional, as you can use an existing cache) 205 | - binding the cache to the function (a MountPoint is used to connect a function being memoized to the cache) 206 | 207 | A cache, an instance of memento.base/Cache, can contain entries from multiple functions and can be shared between memoized functions. 208 | Each memoized function is bound to a Cache via MountPoint. When you call a function such as `(m/as-map a-cached-function)` you are 209 | operating on a MountPoint. 210 | 211 | The reason for this separation is two-fold: 212 | 213 | #### 1. **Improved Size Based Eviction** 214 | 215 | So far all examples implicitly created a new cache for each memoized function, but if we use same cache for multiple 216 | functions, then any size based eviction will apply to them as a whole. If you have 100 memoized functions, and you want to 217 | somewhat limit their memory use, what do you do? In a typical cache library you might limit each of them to 100 entries. So you 218 | allocated 10000 slots total, but one function might have an empty cache, while a very heavily used one needs way more than 100 219 | slots. If all 100 function are backed by same Cache instance with 10000 slots then they automatically balance themselves out. 220 | 221 | #### 2. **Changing cache temporarily to allow for scoped caching** 222 | 223 | This indirection with Mount Points allows us to change which cache is backing a function dynamically. See discussion of tagged 224 | caches below. Here's an example of using tags when caching and scoped caching 225 | 226 | ```clojure 227 | (ns myproject.some-ns 228 | (:require [myproject.cache :as cache] 229 | [memento.core :as m])) 230 | 231 | (defn get-person-by-id [person-id] 232 | (let [person (db/get-person person-id)] 233 | ; tag the returned object with :person + id pair 234 | (m/with-tag-id person :person (:id person)))) 235 | 236 | ; add a cache to the function with tags :person and :request 237 | (m/memo #'get-person-by-id [:person :request] cache/inf) 238 | 239 | ; remove cache entries from every cache tagged :person globally, where the 240 | ; tag is tagged with :person 1 241 | (m/memo-clear-tag! :person 1) 242 | 243 | (m/with-caches :request (constantly (m/create cache/inf)) 244 | ; inside this block, a fresh new cache is used (and discarded) 245 | ; making a scope-like functionality 246 | (get-person-by-id 5)) 247 | ``` 248 | 249 | ## Variable expiry 250 | 251 | Instead of setting a fixed duration of validity for entries in a cache, it is possible 252 | to set these duration on per-tag or per-mount point basis. 253 | 254 | Note that for Caffeine cache variable expiry caching is somewhat slower. 255 | 256 | ### **Read [here](doc/variable-expiry.md)** 257 | 258 | ## Additional features 259 | 260 | #### [Prevent caching of a specific return value (and general return value xform)](doc/ret-fn.md) 261 | #### [Manually add or evict entries](doc/manual-add-remove.md) 262 | 263 | #### `(m/as-map memoized-function)` to get a map of cache entries, also works on MountPoint instances 264 | #### `(m/memoized? a-function)` returns true if the function is memoized 265 | #### `(m/memo-unwrap memoized-function)` returns original uncached function, also works on MountPoint instances 266 | #### `(m/active-cache memoized-function)` returns Cache instance from the function, if present. 267 | 268 | ## Tags 269 | 270 | You can add tags to the caches. Tags enable that you: 271 | 272 | - run actions on caches with specific tags 273 | - **change or update cache of tagged MountPoints within a scope** 274 | - change or update cache of tagged MountPoints permanently 275 | - use secondary index to invalidate entries by a tag + ID pair 276 | 277 | This is a very powerful feature, [read more here.](doc/tags.md) 278 | 279 | ## Loads and invalidations 280 | 281 | Cache only has a single ongoing load for a key going at any one time. For Caffeine cache, if a key is invalidated 282 | during the load, the load is repeated. This is the only way you can get multiple function invocations happen for a single 283 | cached function call. When an tag is invalidated while it's being loaded, the Thread that loads it will be interrupted. 284 | 285 | ## Namespace scan 286 | 287 | You can scan loaded namespaces for annotated vars and automatically create caches. 288 | 289 | [Read more](doc/ns-scan.md) 290 | 291 | ## Events 292 | 293 | You can fire an event at a memoized function. Main use case is to enable adding entries to different functions from same data. 294 | 295 | [Read more](doc/events.md) 296 | 297 | ## Tiered caching 298 | 299 | You can use caches that combine two other caches in some way. The easiest way to generate 300 | the cache configuration needed is to use `memento.core/tiered`,`memento.core/consulting`, `memento.core/daisy`. 301 | 302 | [Read more](doc/tiered.md) 303 | 304 | ## if-cached 305 | 306 | memento.core/if-cache is like an if-let, but the "then" branch executes if the function call 307 | is cached, otherwise else branch is executed. The binding is expected to be a cached function call form, otherwise 308 | an error is thrown. 309 | 310 | Example: 311 | 312 | ```clojure 313 | (if-cached [v (my-function arg1)] 314 | (println "cached value is " v) 315 | (println "value is not cached")) 316 | ``` 317 | 318 | ## Skip/disable caching 319 | 320 | If you set `-Dmemento.enabled=false` JVM option (or change `memento.config/enabled?` var root binding), 321 | then type of all caches created will be `memento.base/no-cache`, which does no caching. 322 | 323 | ## Reload guards 324 | 325 | When you memoize a function with tags, a special object is created that will clean up in internal tag 326 | mappings when memoized function is GCed. It's important when reloading namespaces to remove mount points 327 | on the old function versions. 328 | 329 | It uses finalize, which isn't free (takes extra work to allocate and GC has to work harder), so 330 | if you don't use namespace reloading, and you want to optimize you can disable reload guard objects. 331 | 332 | Set `-Dmemento.reloadable=false` JVM option (or change `memento.config/reload-guards?` var root binding). 333 | 334 | ## Breaking changes 335 | 336 | Patch versions are compatible. Minor version change breaks API for implementation authors, but not for users, 337 | major version change breaks user API. 338 | 339 | Version 1.0.x changed implementation from Guava to Caffeine 340 | Version 0.9.0 introduced many breaking changes. 341 | 342 | ## License 343 | 344 | Copyright © 2020-2021 Rok Lenarčič 345 | 346 | Licensed under the term of the MIT License, see LICENSE. 347 | -------------------------------------------------------------------------------- /test/memento/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns memento.core-test 2 | (:require [clojure.test :refer :all] 3 | [memento.core :as m :refer :all] 4 | [memento.config :as mc] 5 | [memento.caffeine.config :as mcc]) 6 | (:import (java.io IOException) 7 | (memento.base EntryMeta ICache) 8 | (memento.caffeine Expiry) 9 | (memento.mount IMountPoint))) 10 | 11 | (def inf {mc/type mc/caffeine}) 12 | (defn size< [max-size] 13 | (assoc inf mc/size< max-size)) 14 | (defn ret-fn [f] 15 | (assoc inf mc/ret-fn f)) 16 | 17 | (def id (memo identity inf)) 18 | 19 | (defn- check-core-features 20 | [factory] 21 | (let [mine (factory identity) 22 | them (memoize identity)] 23 | (testing "That the memo function works the same as core.memoize" 24 | (are [x y] (= x y) 25 | (mine 42) (them 42) 26 | (mine ()) (them ()) 27 | (mine []) (them []) 28 | (mine #{}) (them #{}) 29 | (mine {}) (them {}) 30 | (mine nil) (them nil))) 31 | (testing "That the memo function has a proper cache" 32 | (is (memoized? mine)) 33 | (is (not (memoized? them))) 34 | (is (= 42 (mine 42))) 35 | (is (not (empty? (into {} (as-map mine))))) 36 | (is (memo-clear! mine)) 37 | (is (empty? (into {} (as-map mine)))))) 38 | (testing "That the cache retries in case of exceptions" 39 | (let [access-count (atom 0) 40 | f (factory 41 | (fn [] 42 | (swap! access-count inc) 43 | (throw (IllegalArgumentException.))))] 44 | (is (thrown? IllegalArgumentException (f))) 45 | (is (thrown? IllegalArgumentException (f))) 46 | (is (= 2 @access-count)))) 47 | (testing "That the memo function does not have a race condition" 48 | (let [access-count (atom 0) 49 | slow-identity 50 | (factory (fn [x] 51 | (swap! access-count inc) 52 | (Thread/sleep 100) 53 | x))] 54 | (every? identity (pvalues (slow-identity 5) (slow-identity 5))) 55 | (is (= @access-count 1)))) 56 | (testing "That exceptions are correctly unwrapped." 57 | (is (thrown? ClassNotFoundException ((factory (fn [] (throw (ClassNotFoundException.))))))) 58 | (is (thrown? IllegalArgumentException ((factory (fn [] (throw (IllegalArgumentException.)))))))) 59 | (testing "Null return caching." 60 | (let [access-count (atom 0) 61 | mine (factory (fn [] (swap! access-count inc) nil))] 62 | (is (nil? (mine))) 63 | (is (nil? (mine))) 64 | (is (= @access-count 1))))) 65 | 66 | (deftest test-memo (check-core-features #(memo % inf))) 67 | 68 | (deftest test-lru 69 | (let [mine (memo identity (size< 2))] 70 | ;; First check that the basic memo behavior holds 71 | (check-core-features #(memo % (size< 2))) 72 | 73 | ;; Now check FIFO-specific behavior 74 | (testing "that when the limit threshold is not breached, the cache works like the basic version" 75 | (are [x y] = 76 | 42 (mine 42) 77 | {[42] 42} (as-map mine) 78 | 43 (mine 43) 79 | {[42] 42, [43] 43} (as-map mine) 80 | 42 (mine 42) 81 | {[42] 42, [43] 43} (as-map mine))) 82 | (testing "that when the limit is breached, the oldest value is dropped" 83 | (are [x y] = 84 | 44 (mine 44) 85 | {[44] 44, [43] 43} (as-map mine))))) 86 | 87 | 88 | (deftest test-ttl 89 | ;; First check that the basic memo behavior holds 90 | (check-core-features #(memo % (assoc inf mc/ttl 2))) 91 | 92 | ;; Now check TTL-specific behavior 93 | (let [mine (memo identity (assoc inf mc/ttl [2 :s]))] 94 | (are [x y] = 95 | 42 (mine 42) 96 | {[42] 42} (as-map mine)) 97 | (Thread/sleep 3000) 98 | (are [x y] = 99 | 43 (mine 43) 100 | {[43] 43} (as-map mine))) 101 | 102 | (let [mine (memo identity (assoc inf mc/ttl [5 :ms])) 103 | limit 2000000 104 | start (System/currentTimeMillis)] 105 | (loop [n 0] 106 | (if-not (mine 42) 107 | (do 108 | (is false (str "Failure on call " n))) 109 | (if (< n limit) 110 | (recur (+ 1 n))))) 111 | (println "ttl test completed" limit "calls in" 112 | (- (System/currentTimeMillis) start) "ms"))) 113 | 114 | (deftest test-memoization-utils 115 | (let [CACHE_IDENTITY (:memento.mount/mount (meta id))] 116 | (testing "that the stored cache is not null" 117 | (is (instance? IMountPoint id))) 118 | (testing "that a populated function looks correct at its inception" 119 | (is (memoized? id)) 120 | (is (instance? ICache (active-cache id))) 121 | (is (as-map id)) 122 | (is (empty? (as-map id)))) 123 | (testing "that a populated function looks correct after some interactions" 124 | ;; Memoize once 125 | (is (= 42 (id 42))) 126 | ;; Now check to see if it looks right. 127 | (is (find (as-map id) '(42))) 128 | (is (= 1 (count (as-map id)))) 129 | ;; Memoize again 130 | (is (= [] (id []))) 131 | (is (find (as-map id) '([]))) 132 | (is (= 2 (count (as-map id)))) 133 | (testing "that upon memoizing again, the cache should not change" 134 | (is (= [] (id []))) 135 | (is (find (as-map id) '([]))) 136 | (is (= 2 (count (as-map id))))) 137 | (testing "if clearing the cache works as expected" 138 | (is (memo-clear! id)) 139 | (is (empty? (as-map id))))) 140 | (testing "that after all manipulations, the cache maintains its identity" 141 | (is (identical? CACHE_IDENTITY (:memento.mount/mount (meta id))))) 142 | (testing "that a cache can be seeded and used normally" 143 | (memo-clear! id) 144 | (is (memo-add! id {[42] 42})) 145 | (is (= 42 (id 42))) 146 | (is (= {[42] 42} (as-map id))) 147 | (is (= 108 (id 108))) 148 | (is (= {[42] 42 [108] 108} (as-map id))) 149 | (is (memo-add! id {[111] nil [nil] 111})) 150 | (is (= 111 (id nil))) 151 | (is (= nil (id 111))) 152 | (is (= {[42] 42 [108] 108 [111] nil [nil] 111} (as-map id)))) 153 | (testing "that we can get back the original function" 154 | (is (memo-clear! id)) 155 | (is (memo-add! id {[42] 24})) 156 | (is (= 24 (id 42))) 157 | (is (= 42 ((memo-unwrap id) 42)))))) 158 | 159 | (deftest memo-with-seed-cmemoize-18 160 | (let [mine (memo identity (assoc inf mc/seed {[42] 99}))] 161 | (testing "that a memo seed works" 162 | (is (= 41 (mine 41))) 163 | (is (= 99 (mine 42))) 164 | (is (= 43 (mine 43))) 165 | (is (= {[41] 41, [42] 99, [43] 43} (as-map mine)))))) 166 | 167 | (deftest memo-with-dropped-args 168 | ;; must use var to preserve metadata 169 | (let [mine (memo + (assoc inf mc/key-fn rest))] 170 | (testing "that key-fnb collapses the cache key space" 171 | (is (= 13 (mine 1 2 10))) 172 | (is (= 13 (mine 10 2 1))) 173 | (is (= 13 (mine 10 2 10))) 174 | (is (= {[2 10] 13, [2 1] 13} (as-map mine)))))) 175 | 176 | (def test-atom (atom 0)) 177 | (defn test-var-fn [x] (swap! test-atom inc) (* x 3)) 178 | 179 | (deftest add-memo-to-var 180 | (testing "that memoing a var works" 181 | (memo #'test-var-fn inf) 182 | (is (= 3 (test-var-fn 1))) 183 | (is (= 3 (test-var-fn 1))) 184 | (is (= 3 (test-var-fn 1))) 185 | (is (= @test-atom 1)))) 186 | 187 | (deftest seed-test 188 | (testing "that seeding a function works" 189 | (let [cached (memo + (assoc inf mc/seed {[3 5] 100 [4 5] 2000}))] 190 | (is (= 50 (cached 20 30))) 191 | (is (= 1 (cached -1 2))) 192 | (is (= 100 (cached 3 5))) 193 | (is (= 2000 (cached 4 5)))))) 194 | 195 | (deftest key-fn-test 196 | (testing "that key-fn works for direct cache" 197 | (let [cached (memo (fn [& ids] ids) (assoc inf mc/key-fn set))] 198 | (is (= [3 2 1] (cached 3 2 1))) 199 | (is (= [3 2 1] (cached 1 2 3))) 200 | (is (= [3 2 1] (cached 1 3 3 2 2 2))) 201 | (is (= [2 1] (cached 2 1)))))) 202 | 203 | (deftest key-fn*-test 204 | (testing "that key-fn works for direct cache" 205 | (let [cached (memo (fn [& ids] ids) (assoc inf mc/key-fn* hash-set))] 206 | (is (= [3 2 1] (cached 3 2 1))) 207 | (is (= [3 2 1] (cached 1 2 3))) 208 | (is (= [3 2 1] (cached 1 3 3 2 2 2))) 209 | (is (= [2 1] (cached 2 1)))))) 210 | 211 | (deftest ret-fn-non-cached 212 | (testing "that ret-fn is ran" 213 | (is (= -4 ((memo + (ret-fn #(* -1 %2))) 2 2))) 214 | (is (= true ((memo (constantly nil) (ret-fn #(nil? %2))) 1))) 215 | (is (= nil ((memo + (ret-fn (constantly nil))) 2 2)))) 216 | (testing "that non-cached is respected" 217 | (let [access-nums (atom []) 218 | f (memo 219 | (fn [number] 220 | (swap! access-nums conj number) 221 | (if (zero? (mod number 3)) (do-not-cache number) number)) 222 | (ret-fn #(if (and (number? %2) (zero? (mod %2 5))) (do-not-cache %2) %2)))] 223 | (is (= (range 20) (map f (range 20)))) 224 | (is (= (range 20) (map f (range 20)))) 225 | (is (= (concat (range 20) [0 3 5 6 9 10 12 15 18]) @access-nums))))) 226 | 227 | (deftest get-tags-test 228 | (testing "tags get returned" 229 | (let [cached (memo identity :person) 230 | cached2 (memo identity [:actor :dog]) 231 | cached3 (memo identity {mc/tags :x})] 232 | (is (= [:person] (tags cached))) 233 | (is (= [:actor :dog] (tags cached2))) 234 | (is (= [:x] (tags cached3)))))) 235 | 236 | (deftest with-caches-test 237 | (testing "a different cache is used within the block" 238 | (let [access-nums (atom []) 239 | f (memo (fn [number] (swap! access-nums conj number)) :person inf)] 240 | (is (= [10] (f 10))) 241 | (is (= [10] (f 10))) 242 | (is (= [10 20] (f 20))) 243 | (is (= [10 20] (f 20))) 244 | (is (= [10 20] @access-nums)) 245 | (with-caches :person (constantly (create inf)) 246 | (is (= [10 20 10] (f 10))) 247 | (is (= [10 20 10] (f 10))) 248 | (is (= [10 20 10 30] (f 30))) 249 | (is (= [10 20 10 30] @access-nums))) 250 | (is (= [10] (f 10))) 251 | (is (= [10 20 10 30 30] (f 30)))))) 252 | 253 | (deftest update-tag-caches-test 254 | (testing "changes cache root binding" 255 | (let [access-nums (atom 0) 256 | f (memo (fn [number] (swap! access-nums + number)) :person inf)] 257 | (is (= 10 (f 10))) 258 | (is (= 10 (f 10))) 259 | (is (= 10 @access-nums)) 260 | (update-tag-caches! :person (constantly (create inf))) 261 | (is (= 20 (f 10))) 262 | (is (= 20 @access-nums)) 263 | (with-caches :person (constantly (create inf)) 264 | (is (= 30 (f 10))) 265 | (is (= 30 (f 10))) 266 | (is (= 30 @access-nums)) 267 | (update-tag-caches! :person (constantly (create inf))) 268 | (is (= 40 (f 10))) 269 | (is (= 40 @access-nums))) 270 | (is (= 20 (f 10))) 271 | (is (= 40 @access-nums)) 272 | (update-tag-caches! :person (constantly (create inf))) 273 | (is (= 50 (f 10))) 274 | (is (= 50 @access-nums))))) 275 | 276 | (deftest tagged-eviction-test 277 | (testing "adding tag ID info" 278 | (is (= (EntryMeta. 1 false #{[:person 55]}) 279 | (-> 1 (with-tag-id :person 55)))) 280 | (is (= (EntryMeta. 1 true #{[:person 55] [:account 6]}) 281 | (-> 1 (with-tag-id :person 55) (with-tag-id :account 6) do-not-cache)))) 282 | (testing "tagged eviction" 283 | (let [f (memo (fn [x] (with-tag-id x :tag x)) :tag inf)] 284 | (is (= {} (as-map f))) 285 | (is (= {[1] 1} (do (f 1) (as-map f)))) 286 | (is (= {[1] 1 [2] 2} (do (f 2) (as-map f)))) 287 | (is (= {[2] 2} (do (memo-clear-tag! :tag 1) (as-map f))))))) 288 | 289 | (deftest fire-event-test 290 | (testing "event is fired on referenced cache" 291 | (let [access-nums (atom 0) 292 | inner-f (fn [x] (swap! access-nums inc) x) 293 | evt-f (fn [this evt] 294 | (m/memo-add! this {[evt] (inc evt)})) 295 | x (m/memo inner-f {mc/type mc/caffeine mc/evt-fn evt-f mc/tags [:a]}) 296 | y (m/memo inner-f {mc/type mc/caffeine mc/evt-fn evt-f})] 297 | (is (= 1 (x 1))) 298 | (is (= 1 (x 1))) 299 | (is (= 1 @access-nums)) 300 | (m/fire-event! x 4) 301 | (m/fire-event! :a 5) 302 | (m/fire-event! y 6) 303 | (is (= {[1] 1 304 | [4] 5 305 | [5] 6} (m/as-map x))) 306 | (is (= {[6] 7} (m/as-map y))) 307 | (is (= 5 (x 4))) 308 | (is (= 6 (x 5))) 309 | (is (= 7 (y 6))) 310 | (is (= 1 @access-nums))))) 311 | 312 | (deftest if-cached-test 313 | (testing "if-cached executes then when cached" 314 | (let [x (m/memo identity {mc/type mc/caffeine})] 315 | (x 2) 316 | (is (= 2 317 | (m/if-cached [y (x 2)] 318 | y 319 | (throw (ex-info "Shouldn't throw" {}))))))) 320 | (testing "if-cached executes else when not cached" 321 | (let [x (m/memo identity {mc/type mc/caffeine})] 322 | (is (= ::none 323 | (m/if-cached [y (x 2)] 324 | (throw (ex-info "Shouldn't throw" {})) 325 | ::none)))))) 326 | 327 | (deftest put-during-load-test 328 | (testing "adding entries during load" 329 | (let [c (m/create inf) 330 | fn1 (m/memo identity {} c) 331 | fn2 (m/memo (fn [x] (m/memo-add! fn1 {[x] (inc x)}) 332 | (dec x)))] 333 | (is (= 4 (fn2 5))) 334 | (is (= 6 (fn1 5)))))) 335 | 336 | (defn fib [x] (if (<= x 1) 1 (+ (fib (- x 2)) (fib (dec x))))) 337 | 338 | (memo #'fib inf) 339 | 340 | (defn recursive [x] (recursive x)) 341 | 342 | (memo #'recursive inf) 343 | 344 | (deftest recursive-test 345 | (testing "recursive loads" 346 | (is (= 20365011074 (fib 50))) 347 | (is (thrown? StackOverflowError (recursive 1))))) 348 | 349 | (deftest concurrent-load 350 | (testing "concurrent test" 351 | (let [cnt (atom 0) 352 | f (m/memo (fn [x] 353 | (Thread/sleep 1000) 354 | (swap! cnt inc) x) 355 | inf) 356 | v (doall (repeatedly 5 #(future (f 1))))] 357 | (is (= [1 1 1 1 1] (mapv deref v)))))) 358 | 359 | (deftest vectors-key-fn* 360 | (testing "vectors don't throw exception when used with key-fn*" 361 | (let [c (m/memo identity (assoc inf mc/key-fn* identity))] 362 | (is (some? (m/memo-add! c {[1] 2})))))) 363 | 364 | (deftest invalidation-during-load-test 365 | (testing "bulk invalidation test" 366 | (let [a (atom 0) 367 | c (m/memo (fn [] (Thread/sleep 300) 368 | (m/with-tag-id (swap! a inc) :xx 1)) 369 | (assoc inf mc/tags :xx))] 370 | (future (Thread/sleep 15) 371 | (m/memo-clear-tag! :xx 1)) 372 | (is (= 2 (c))))) 373 | (testing "Invalidation during load test" 374 | (let [a (atom 0) 375 | after (atom 0) 376 | c (m/memo (fn [] (let [r (swap! a inc)] 377 | (Thread/sleep 300) 378 | [r (swap! after inc)])) inf)] 379 | (future (Thread/sleep 10) 380 | (m/memo-clear! c)) 381 | (is (= [2 1] (c)))))) 382 | 383 | (deftest ret-ex-fn-test 384 | (testing "returns transformed-exception" 385 | (let [e (RuntimeException.) 386 | c (m/memo (fn [] (Thread/sleep 100) 387 | (throw (IOException.))) 388 | (assoc inf mc/ret-ex-fn (fn [_ ee] (when (instance? IOException ee) e)))) 389 | f1 (future (try (c) (catch Exception e e))) 390 | f2 (future (try (c) (catch Exception e e)))] 391 | (is (= e @f1)) 392 | (is (= e @f2))))) 393 | 394 | (deftest variable-expiry-test 395 | (testing "Variable expiry" 396 | (let [c (m/memo 397 | identity 398 | (assoc inf mcc/expiry 399 | (reify Expiry 400 | (ttl [this _ k v] v) 401 | (fade [this _ k v]))))] 402 | (c 1) 403 | (c 2) 404 | (c 3) 405 | (Thread/sleep 1100) 406 | (is (= {'(2) 2 '(3) 3} (m/as-map c))))) 407 | (testing "Variable expiry fade" 408 | (let [c (m/memo 409 | identity 410 | (assoc inf mcc/expiry 411 | (reify Expiry 412 | (ttl [this _ k v] ) 413 | (fade [this _ k v] v))))] 414 | (c 1) 415 | (c 2) 416 | (c 3) 417 | (Thread/sleep 1100) 418 | (is (= {'(2) 2 '(3) 3} (m/as-map c))))) 419 | (testing "variable expiry via meta" 420 | (let [c (m/memo 421 | #(with-meta {} {mc/ttl (long (+ 1 %))}) 422 | (assoc inf mcc/expiry mcc/meta-expiry))] 423 | (c 1) 424 | (c 2) 425 | (c 3) 426 | (Thread/sleep 1100) 427 | (is (= {'(1) {} '(2) {} '(3) {}} (m/as-map c))) 428 | (Thread/sleep 1000) 429 | (is (= {'(2) {} '(3) {}} (m/as-map c)))))) --------------------------------------------------------------------------------
9 | * Most functions receive a Segment object that should be used to partition for different functions 10 | * and using other : 11 | * - id: use for separating caches, it is either name specified by user's config, or var name or function object 12 | * - key-fn: key-fn from mount point, use this to generate cache key 13 | * - f: use this function to load values 14 | */ 15 | public interface ICache { 16 | /** 17 | * Return the conf for this cache. 18 | * 19 | * @return 20 | */ 21 | IPersistentMap conf(); 22 | 23 | /** 24 | * Return the cache value. 25 | *
26 | * - segment is Segment record provided by the mount point, it contains information that allows Cache 27 | * to separate caches for different functions 28 | * 29 | * @param segment 30 | * @param args 31 | * @return 32 | */ 33 | Object cached(Segment segment, ISeq args); 34 | 35 | /** 36 | * Return cached value if present (and available immediately) in cache or memento.base/absent otherwise. 37 | * 38 | * @param segment 39 | * @param args 40 | * @return 41 | */ 42 | Object ifCached(Segment segment, ISeq args); 43 | 44 | /** 45 | * Invalidate all the entries linked a mount's single arg list, return Cache 46 | * 47 | * @param segment 48 | * @return 49 | */ 50 | ICache invalidate(Segment segment); 51 | 52 | /** 53 | * Invalidate all the entries linked to a mount, return Cache 54 | * 55 | * @param segment 56 | * @param args 57 | * @return 58 | */ 59 | ICache invalidate(Segment segment, ISeq args); 60 | 61 | /** 62 | * Invalidate all entries, returns Cache 63 | * 64 | * @return 65 | */ 66 | ICache invalidateAll(); 67 | 68 | /** 69 | * Invalidate entries with these secondary IDs, returns Cache. Each ID is a pair of tag and object 70 | * 71 | * @param id 72 | * @return 73 | */ 74 | ICache invalidateIds(Iterable id); 75 | 76 | /** 77 | * Add entries as for a function 78 | * 79 | * @param segment 80 | * @param argsToVals 81 | * @return 82 | */ 83 | ICache addEntries(Segment segment, IPersistentMap argsToVals); 84 | 85 | /** 86 | * Return all entries in the cache with keys shaped like as per cache implementation. 87 | * 88 | * @return 89 | */ 90 | IPersistentMap asMap(); 91 | 92 | /** 93 | * Return all entries in the cache for a mount with keys shaped like as per cache implementation. 94 | * 95 | * @param segment 96 | * @return 97 | */ 98 | IPersistentMap asMap(Segment segment); 99 | } 100 | -------------------------------------------------------------------------------- /doc/major.md: -------------------------------------------------------------------------------- 1 | # Caches and memoize calls 2 | 3 | ### Intro 4 | 5 | We've seen `defmemo` macro that defines a function, creates a cache based on conf, then attaches that cache. 6 | It is a combination of a `defn` to define a function and a `memento.core/memo` call to create a cache and bind it. 7 | 8 | And `memo` itself is a combination of: 9 | 10 | - creating a Cache via `memento.core/create` (optional, as you can use an existing cache) 11 | - binding the cache to the function via `memento.core/bind` (a MountPoint is used to connect a function being memoized to the cache) 12 | 13 | When `memo` or equivalent is called with a conf map, a new cache will be created and bound, if a `memento.base/Cache` instance 14 | is given that will be used instead of creating a new cache from a conf map. After `memo` or `bind` the function has a MountPoint attached. 15 | 16 | #### Creating a cache 17 | 18 | Creating a cache is done by using `memento.core/create`, which takes a map of configuration (called **cache conf**). 19 | You can use the resulting Cache with multiple functions. The configuration properties (map keys) can be found 20 | in `memento.config` and `memento.caffeine.config`, look for "Cache setting" in docstring. 21 | 22 | If `memento.config/enabled?` is false, this function always returns `memento.base/no-cache`, which is a Cache 23 | implementation that doesn't do any caching. You can set this at start-up by specifying java property: 24 | `-Dmemento.enabled=false` which globally disables caching. 25 | 26 | #### Binding the cache 27 | 28 | Binding the cache to a function is done by `memento.core/bind`. Parameters are: 29 | 30 | - a fn or a var, if var, the root value of var is changed to a memoized version 31 | - a mount point configuration or **mount conf** for short 32 | - a Cache instance that you want to bind 33 | 34 | Mount conf is either a map of mount point configuration properties, or a shorthand (see below). 35 | The configuration properties (map keys) can be found in `memento.config`, look for "function bind" in docstring. 36 | 37 | Instead of map of properties, **mount conf** can be a shorthand, which has the following two shorthands: 38 | - `[:some-keyword :another-keyword]` -> `{:memento.core/tags [:some-keyword :another-keyword]}` 39 | - `:a-keyword` -> `{:memento.core/tags [:a-keyword]}` 40 | 41 | #### Create + bind combined 42 | 43 | You can combine both functions into 1 call using `memento.core/memo`. 44 | 45 | ```clojure 46 | (m/memo fn-or-var mount-conf cache-conf) 47 | ``` 48 | 49 | To make things shorter, there's a 2-arg variant that allows that you specify both configurations at once: 50 | 51 | ```clojure 52 | (m/memo fn-or-var conf) 53 | ``` 54 | 55 | If conf is a map, then all the properties valid for mount conf are treated as such. The rest is passed to cache create. 56 | If conf is a mount conf shorthand then cache conf is considered to be {}. E.g. 57 | 58 | ```clojure 59 | (m/memo my-fn :my-tag) 60 | ``` 61 | 62 | This creates a memoized function tagged with `:my-tag` bound to a cache that does no caching. 63 | -------------------------------------------------------------------------------- /test/memento/caffeine_test.clj: -------------------------------------------------------------------------------- 1 | (ns memento.caffeine-test 2 | (:require [clojure.test :refer :all] 3 | [memento.base :as b] 4 | [memento.core :as m] 5 | [memento.config :as mc] 6 | [memento.caffeine :refer :all] 7 | [memento.caffeine.config :as mcc]) 8 | (:import (memento.base CacheKey))) 9 | 10 | #_(deftest cache-creation 11 | (testing "Creates a cache builder" 12 | (are [expected props] 13 | (= expected (str (conf->builder props nil))) 14 | "Caffeine{initialCapacity=11, removalListener}" {mc/initial-capacity 11} 15 | "Caffeine{maximumSize=29, removalListener}" {mc/size< 29} 16 | "Caffeine{maximumWeight=30, removalListener}" {mcc/weight< 30} 17 | "Caffeine{expireAfterWrite=35000000000ns, removalListener}" {mc/ttl 35} 18 | "Caffeine{expireAfterWrite=2100000000000ns, removalListener}" {mc/ttl [35 :m]} 19 | "Caffeine{expireAfterAccess=36000000000ns, removalListener}" {mc/fade 36} 20 | "Caffeine{expireAfterAccess=36000000ns, removalListener}" {mc/fade [36 :ms]} 21 | ;;"Caffeine{removalListener}" {::/refresh 37} 22 | ;;"Caffeine{removalListener}" {::mg/refresh [37 :d]} 23 | "Caffeine{removalListener}" {mcc/stats true} 24 | "Caffeine{removalListener}" {mcc/kv-weight (fn [f k v] 1)} 25 | "Caffeine{keyStrength=weak, removalListener}" {mcc/weak-keys true} 26 | "Caffeine{valueStrength=weak, removalListener}" {mcc/weak-values true} 27 | "Caffeine{valueStrength=soft, removalListener}" {mcc/soft-values true} 28 | "Caffeine{removalListener}" {mcc/ticker (fn [] (System/nanoTime))} 29 | "Caffeine{removalListener}" {mcc/removal-listener (fn [k v event] nil)} 30 | "Caffeine{initialCapacity=11, maximumWeight=30, expireAfterWrite=100000000000ns, expireAfterAccess=111000000000ns, keyStrength=weak, valueStrength=weak, removalListener}" 31 | {mc/initial-capacity 11 32 | mcc/weight< 30 33 | mc/ttl 100 34 | mc/fade 111 35 | mcc/stats true 36 | mcc/weak-keys true 37 | mcc/weak-values true 38 | mcc/removal-listener (fn [k v event] nil)})) 39 | (testing "Creates a working cache" 40 | (let [a (atom 0) 41 | builder (conf->builder {mcc/weight< 34 42 | mcc/kv-weight (fn [f k v] 20) 43 | mcc/ticker (fn [] (System/nanoTime)) 44 | mcc/removal-listener (fn [f k v event] nil)} 45 | nil) 46 | cache (.build builder)] 47 | (is (= 2 (.get cache (->CacheKey identity [1]) (java8function (fn [_] 2)))))))) 48 | 49 | (deftest data-loading-unloading 50 | (testing "Serializes" 51 | (let [c (m/memo identity {mc/type mc/caffeine mc/id "A"})] 52 | (c 1) 53 | (is (= (to-data (m/active-cache c)) {["A" '(1)] 1})))) 54 | (testing "Deserializes" 55 | (let [c (m/memo identity {mc/type mc/caffeine mc/id "A"})] 56 | (load-data (m/active-cache c) {["X" '(4)] 5}) 57 | (is (= (b/as-map (m/active-cache c)) 58 | {(CacheKey. "X" [4]) 5}))))) 59 | -------------------------------------------------------------------------------- /test/memento/mount_test.clj: -------------------------------------------------------------------------------- 1 | (ns memento.mount-test 2 | (:require [memento.mount :as m] 3 | [memento.base :as b] 4 | [memento.config :as mc] 5 | [memento.core :as core] 6 | [clojure.test :refer :all])) 7 | 8 | (deftest cache-tags-test 9 | (testing "Add tags" 10 | (is (= #:memento.mount-test{:a #{1} :b #{1} :c #{1}} 11 | (m/assoc-cache-tags {} [::a ::b ::c] 1))) 12 | (is (= #:memento.mount-test{:a #{2 3 4} :b #{2} :c #{2 3 4} :d #{2}} 13 | (-> {} 14 | (m/assoc-cache-tags [::a ::b ::c] 2) 15 | (m/assoc-cache-tags [::a ::c] 3) 16 | (m/assoc-cache-tags [::a ::c] 4) 17 | (m/assoc-cache-tags [::a ::c ::d] 2))))) 18 | (testing "Remove tags" 19 | (is (= #:memento.mount-test{:a #{3 4} :b #{} :c #{3 4} :d #{}} 20 | (-> {} 21 | (m/assoc-cache-tags [::a ::b ::c] 2) 22 | (m/assoc-cache-tags [::a ::c] 3) 23 | (m/assoc-cache-tags [::a ::c] 4) 24 | (m/assoc-cache-tags [::a ::c ::d] 2) 25 | (m/dissoc-cache-tags 2)))))) 26 | 27 | (deftest reify-mount-conf-test 28 | (is (= {:a 1 :B 3} (m/reify-mount-conf {:a 1 :B 3}))) 29 | (is (= {:memento.core/tags [1]} (m/reify-mount-conf 1))) 30 | (is (= {:memento.core/tags [1 2 3]} (m/reify-mount-conf [1 2 3])))) 31 | 32 | (deftest update-existing-test 33 | (is (= {:a 1 :b 2} (m/update-existing {:a 1 :b 1} [:b :c] inc)))) 34 | 35 | (defn test-fn [x] nil) 36 | (def test-fn-saved test-fn) 37 | 38 | (deftest id-test 39 | (let [x inc] 40 | (are [expected-id fn-or-var conf] 41 | (= expected-id (-> (m/bind fn-or-var conf b/no-cache) .getMp .segment .getId)) 42 | test-fn-saved test-fn {} 43 | test-fn-saved test-fn :a 44 | "#'memento.mount-test/test-fn" #'test-fn {} 45 | :x #'test-fn {mc/id :x} 46 | :x test-fn {mc/id :x}))) 47 | 48 | (def a (atom 0)) 49 | 50 | (defn add-prefix 51 | [x] 52 | (swap! a inc) 53 | (str "prefix-" x)) 54 | 55 | (defn add-suffix 56 | [x] 57 | (swap! a + 10) 58 | (str (add-prefix x) "-suffix")) 59 | 60 | (core/memo #'add-prefix {mc/tags [:test]}) 61 | (core/memo #'add-suffix {mc/tags [:test]}) 62 | 63 | (defn fib 64 | [x] 65 | (if (<= x 1) 1 (+ (fib (dec x)) (fib (dec (dec x)))))) 66 | 67 | (core/memo #'fib {mc/tags [:test]}) 68 | 69 | (deftest recursive-call-test 70 | (testing "Call same cache recursively." 71 | (let [_ (reset! a 0) 72 | c (core/create {mc/type mc/caffeine})] 73 | (is (= "prefix-A-suffix" 74 | (core/with-caches 75 | :test (constantly c) 76 | (add-suffix "A") 77 | (add-suffix "A") 78 | (add-suffix "A")))) 79 | (is (= @a 11)))) 80 | (testing "Call same cache function recursively." 81 | (let [c (core/create {mc/type mc/caffeine 82 | mc/initial-capacity 4})] 83 | (core/with-caches 84 | :test (constantly c) 85 | (is (= 10946 (fib 20))))))) 86 | 87 | (defmulti odd-even (fn [x] (odd? x))) 88 | (defonce orig-multi-fn odd-even) 89 | 90 | (core/memo #'odd-even {mc/type mc/caffeine}) 91 | 92 | (deftest multi-method-test 93 | (let [access-count (atom 0)] 94 | (testing "Allows multimethod utilites" 95 | (defmethod odd-even true [x] 96 | (swap! access-count inc) 97 | (println x " is odd")) 98 | (remove-all-methods odd-even) 99 | (defmethod odd-even false [x] 100 | (swap! access-count inc) 101 | (str x " is even")) 102 | (is (thrown? Exception (odd-even 1))) 103 | (is (= "2 is even" (odd-even 2)) ) 104 | (is (some? (get-method odd-even false))) 105 | (is (nil? (get-method odd-even true)))) 106 | (testing "Is cached" 107 | (odd-even 2) 108 | (odd-even 2) 109 | (odd-even 2) 110 | (is (= 1 @access-count)) 111 | (is (= orig-multi-fn (core/memo-unwrap odd-even))) 112 | (is (= true (core/memoized? odd-even)))))) -------------------------------------------------------------------------------- /src/memento/guava/config.clj: -------------------------------------------------------------------------------- 1 | (ns memento.guava.config 2 | "Guava implementation config helpers. 3 | 4 | Contains documented definitions of standard options of Guava cache config." 5 | {:author "Rok Lenarčič"}) 6 | 7 | (def ^:deprecated removal-listener 8 | "Cache setting, corresponds to .removalListener on CacheBuilder. 9 | 10 | A function of four arguments (fn [f key value removal-cause] nil), 11 | that will be called whenever an entry is removed. 12 | 13 | The four arguments are: 14 | 15 | - the function being cached 16 | - the key (arg-list transformed by key-fn if any) 17 | - the value (after ret-fn being applied) 18 | - com.google.common.cache.RemovalCause 19 | 20 | Warning: any exception thrown by listener will not be propagated to the Cache user, only logged via a Logger." 21 | (identity :memento.caffeine/removal-listener)) 22 | 23 | (def ^:deprecated weight< 24 | "Cache setting, a long. 25 | 26 | Specifies the maximum weight of entries the cache may contain. If using this option, 27 | you must provide `kw-weight` option for cache to calculate the weight of entries. 28 | 29 | A cache may evict entries before the specified limit is reached." 30 | (identity :memento.caffeine/weight<)) 31 | 32 | (def ^:deprecated kv-weight 33 | "Cache setting, a function of 3 arguments (fn [f key value] int-weight), 34 | that will be used to determine the weight of entries. 35 | 36 | It should return an int, the weight of the entry. 37 | 38 | The 3 arguments are: 39 | - the first argument is the function being cached 40 | - the second argument is the key (arg-list transformed by key-fn if any) 41 | - the third argument is the value (after ret-fn being applied)" 42 | (identity :memento.caffeine/kv-weight)) 43 | 44 | (def ^:deprecated weak-keys 45 | "Cache setting, corresponds to .weakKeys on CacheBuilder. 46 | 47 | Boolean flag, enabling storing keys using weak references. 48 | 49 | Specifies that each key (not value) stored in the cache should be wrapped in a WeakReference 50 | (by default, strong references are used). 51 | 52 | Warning: when this method is used, the resulting cache will use identity (==) comparison 53 | to determine equality of keys. Its Cache.asMap() view will therefore technically violate 54 | the Map specification (in the same way that IdentityHashMap does). 55 | 56 | The identity comparison makes this not very useful." 57 | (identity :memento.caffeine/weak-keys)) 58 | 59 | (def ^:deprecated weak-values 60 | "Cache setting, corresponds to .weakValues on CacheBuilder. 61 | 62 | Boolean flag, enabling storing values using weak references. 63 | 64 | This allows entries to be garbage-collected if there are no other (strong or soft) references to the values." 65 | (identity :memento.caffeine/weak-values)) 66 | 67 | (def ^:deprecated soft-values 68 | "Cache setting, corresponds to .softValues on CacheBuilder. 69 | 70 | Boolean flag, enabling storing values using soft references. 71 | 72 | Softly referenced objects are garbage-collected in a globally least-recently-used manner, 73 | in response to memory demand. Because of the performance implications of using soft references, 74 | we generally recommend using the more predictable maximum cache size instead." 75 | (identity :memento.caffeine/soft-values)) 76 | 77 | (def ^:deprecated stats 78 | "Cache setting, boolean flag, enabling collection of stats. 79 | 80 | Corresponds to .enableStats on CacheBuilder. 81 | 82 | You can retrieve a cache's stats by using memento.caffeine/stats. 83 | 84 | Returns com.google.common.cache.CacheStats instance or nil." 85 | (identity :memento.caffeine/stats)) 86 | 87 | (def ^:deprecated ticker 88 | "Cache setting, corresponds to .ticker on CacheBuilder. 89 | 90 | A function of zero arguments that should return current nano time. 91 | This is used when doing time based eviction. 92 | 93 | The default is (fn [] (System/nanoTime)). 94 | 95 | This is useful for testing and you can also make the time move in discrete amounts (e.g. you can 96 | make all cache accesses in a request have same time w.r.t. eviction)." 97 | (identity :memento.caffeine/ticker)) 98 | -------------------------------------------------------------------------------- /java/memento/caffeine/SecondaryIndex.java: -------------------------------------------------------------------------------- 1 | package memento.caffeine; 2 | 3 | import clojure.lang.ISeq; 4 | import memento.base.CacheKey; 5 | import memento.base.EntryMeta; 6 | 7 | import java.lang.ref.ReferenceQueue; 8 | import java.lang.ref.WeakReference; 9 | import java.util.HashSet; 10 | import java.util.Objects; 11 | import java.util.Set; 12 | import java.util.concurrent.ConcurrentHashMap; 13 | import java.util.function.Consumer; 14 | 15 | public class SecondaryIndex { 16 | 17 | private final ConcurrentHashMap> lookup; 18 | 19 | public SecondaryIndex(int concurrency) { 20 | this.lookup = new ConcurrentHashMap<>(16, 0.75f, concurrency); 21 | } 22 | 23 | /** 24 | * Add entry to secondary index. 25 | * k is CacheKey of incoming Cache entry 26 | * v is value of incoming cache entry, might be EntryMeta, if it is then we use each tag-idents 27 | * as key (id) pointing to a HashSet of CacheKeys. 28 | * 29 | * For each ID we add CacheKey to its HashSet. 30 | * 31 | * @param k 32 | * @param v 33 | */ 34 | public void add(CacheKey k, Object v) { 35 | if (v instanceof EntryMeta) { 36 | EntryMeta e = ((EntryMeta) v); 37 | ISeq s = e.getTagIdents().seq(); 38 | while (s != null) { 39 | Set cacheKeys = lookup.computeIfAbsent(s.first(), key -> new HashSet<>()); 40 | synchronized (cacheKeys) { 41 | cacheKeys.add(new IndexEntry(cacheKeys, k)); 42 | } 43 | s = s.next(); 44 | } 45 | } 46 | } 47 | 48 | public void drainKeys(Object tagId, Consumer onValue) { 49 | Set entries = lookup.remove(tagId); 50 | if (entries != null) { 51 | synchronized (entries) { 52 | for (IndexEntry e : entries) { 53 | CacheKey c = e.get(); 54 | if (c != null) { 55 | onValue.accept(c); 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | 63 | private static final ReferenceQueue evicted = new ReferenceQueue<>(); 64 | 65 | public static class IndexEntry extends WeakReference { 66 | private final Set home; 67 | private final int hash; 68 | 69 | public IndexEntry(Set home, CacheKey key) { 70 | super(key, evicted); 71 | this.hash = key.hashCode(); 72 | this.home = home; 73 | } 74 | 75 | public void delete() { 76 | synchronized (home) { 77 | home.remove(this); 78 | } 79 | } 80 | 81 | @Override 82 | // this is only used when adding entries, so we can expect this to have the underlying key here 83 | public boolean equals(Object o) { 84 | if (this == o) return true; 85 | if (o instanceof IndexEntry) { 86 | IndexEntry that = (IndexEntry) o; 87 | return hash == that.hash && Objects.equals(get(), that.get()); 88 | } else { 89 | return false; 90 | } 91 | } 92 | 93 | @Override 94 | // this is special hashcode, it remembers the key object's hash so we can kinda use hashset 95 | public int hashCode() { 96 | return hash; 97 | } 98 | } 99 | 100 | public static class Cleaner implements Runnable { 101 | 102 | @Override 103 | public void run() { 104 | while (true) { 105 | try { 106 | IndexEntry ref = (IndexEntry) evicted.remove(); 107 | ref.delete(); 108 | } catch (InterruptedException e) { 109 | // 110 | } 111 | } 112 | } 113 | } 114 | 115 | public static final Thread cleanerThread = new Thread(new Cleaner(), "Memento Secondary Index Cleaner"); 116 | 117 | static { 118 | cleanerThread.setDaemon(true); 119 | cleanerThread.start(); 120 | } 121 | 122 | 123 | } 124 | -------------------------------------------------------------------------------- /doc/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | You can fire an event at a memoized function. The target can be a particular function (or MountPoint), or 3 | you can specify a tag (and all tagged functions get the event). Each function can configure its own handler for 4 | events. Event can be any object, I suggest you use a structure that will enable event handlers to distinguish 5 | events. 6 | 7 | Event handler is a function of two arguments, the MountPoint it's been triggered (most core functions work on those) 8 | and the event. 9 | 10 | Main use case is to enable adding entries to different functions from same data. Example: 11 | 12 | ```clojure 13 | (defn get-project-name 14 | "Returns project name" 15 | [project-id]) 16 | 17 | (m/memo #'get-project-name inf) 18 | 19 | (defn get-project-owner 20 | "Returns project's owner user ID" 21 | [project-id]) 22 | 23 | (m/memo #'get-project-owner inf) 24 | 25 | (defn get-user-projects 26 | "Returns a big expensive list" 27 | [user-id] 28 | (let [project-list '...] 29 | project-list)) 30 | ``` 31 | 32 | In that example, when `get-user-projects` is called, we might load over a 100 projects, and we'd hate to waste that 33 | and not inform `get-project-name` and `get-project-owner` about the facts we've established here, especially since we 34 | might be calling these smaller functions in a loop right after fetching the big list. 35 | 36 | Here's a way to make sure data is reused by manually pushing entries into the caches as supported by most caching libs: 37 | 38 | ```clojure 39 | (defn get-user-projects 40 | "Returns a big expensive list" 41 | [user-id] 42 | (let [project-list '...] 43 | ;; preload entries for seen projects into caches 44 | (m/memo-add! get-project-name 45 | (zipmap (map (comp list :id) project-list) 46 | (map :name project-list))) 47 | (m/memo-add! get-project-owner 48 | (zipmap (map (comp list :id) project-list) 49 | (repeat user-id))) 50 | project-list)) 51 | ``` 52 | 53 | The problem with this solution is that it is an absolute nightmare to maintain: 54 | - adding/removing data consuming functions like `get-project-name` means that I have to also fix producing 55 | functions like `get-user-projects` 56 | - worse yet, the producer function has to be aware of what the argument list of consuming function looks like 57 | and how the output of that function is related to that. For instance if I change arg list for `get-project-owner` 58 | I must fix the `get-user-projects` code that pushes cache entries 59 | - if I want additional producers like `get-user-projects` then each such producer must implement all these changes 60 | and each has a massive block to feed all the consumers 61 | 62 | 63 | I can use events instead and co-locate the code that feeds the cache with the function: 64 | 65 | ```clojure 66 | (defn get-project-name 67 | "Returns project name" 68 | [project-id]) 69 | 70 | (m/memo #'get-project-name 71 | (assoc inf 72 | mc/evt-fn (m/evt-cache-add 73 | :project-seen 74 | (fn [{:keys [name id]}] {[id] name})) 75 | mc/tags [:project])) 76 | 77 | (defn get-project-owner 78 | "Returns project's owner user ID" 79 | [project-id]) 80 | 81 | (m/memo #'get-project-owner 82 | (assoc inf 83 | mc/evt-fn (m/evt-cache-add 84 | :project-seen 85 | (fn [{:keys [id user-id]}] {[id] user-id})) 86 | mc/tags [:project])) 87 | 88 | (defn get-user-projects 89 | "Returns a big expensive list" 90 | [user-id] 91 | (let [project-list '...] 92 | (doseq [p project-list] 93 | (m/fire-event! :project [:project-seen (assoc p :user-id user-id)])) 94 | project-list)) 95 | ``` 96 | 97 | We're using the `evt-cache-add` convenience function that assumes event shape is a 98 | vector of type + payload and that the intent is to add entries to the cache. 99 | 100 | In this case the producer function is only concerned with firing events at tagged caches. 101 | It doesn't need to consider the number of shape of consumers. 102 | 103 | The caching declaration of consumer functions is where there the cache feeding logic is located, 104 | which makes things manageable. 105 | -------------------------------------------------------------------------------- /doc/tags.md: -------------------------------------------------------------------------------- 1 | # Tags 2 | 3 | You can add tags to the caches. You can run actions on caches with specific tags. 4 | 5 | You can specify them via `:memento.core/tags` key (also `mc/tags` value), 6 | or you can simply specify them instead of conf map, which creates a tagged cache 7 | of noop type (that you can replace later). 8 | 9 | ```clojure 10 | (m/memo {mc/tags [:request-scope :person]} #'get-person-by-id) 11 | (m/memo [:request-scope :person] #'get-person-by-id) 12 | (m/memo :person #'get-person-by-id) 13 | ``` 14 | 15 | #### Utility 16 | 17 | You can fetch tags on a memoized function. 18 | 19 | ```clojure 20 | (m/tags get-person-by-id) 21 | => [:person] 22 | ``` 23 | 24 | You can fetch all mount points of functions that are tagged by a specific tag: 25 | 26 | ```clojure 27 | (m/mounts-by-tag :person) 28 | => #{#memento.mount.TaggedMountPoint{...}} 29 | ``` 30 | 31 | #### Change / update cache within a scope 32 | 33 | ```clojure 34 | (m/with-caches :person (constantly (m/create cache/inf-cache)) 35 | (get-person-by-id db-spec 1 12) 36 | (get-person-by-id db-spec 1 12) 37 | (get-person-by-id db-spec 1 12)) 38 | ``` 39 | 40 | Every memoized function (mountpoint) inside the block has its cache updated to the result of the 41 | provided function. In this example, all the `:person` tagged functions will use the same unbounded cache 42 | within the block. This effectively stops them from using any previously cached values and any values added to 43 | cache are dropped when block is exited. 44 | 45 | **This is extremely useful to achieve request scoped caching.** 46 | 47 | #### Updating / changing cache instance permanently 48 | 49 | You can update Cache instances of all functions tagged by a specific tag. This will modify root binding 50 | if not inside `with-caches`, otherwise it will modify the binding. 51 | 52 | ```clojure 53 | (m/update-tag-caches! :person (constantly (m/create cache/inf-cache))) 54 | ``` 55 | 56 | All `:person` tagged memoized functions will from this point on use a new empty unbounded cache. 57 | 58 | #### Applying operations to tagged memoized functions 59 | 60 | Use `mounts-by-tag` to grab mount points and then apply any of the core functions to them. 61 | 62 | ```clojure 63 | (doseq [f (m/mounts-by-tag :person)] 64 | (m/memo-clear! f)) 65 | ``` 66 | 67 | #### Invalidate entries by a tag + ID combo 68 | 69 | You can add tag + ID pairs to cached values. This can be later used to invalidate these 70 | entried based on that ID. 71 | 72 | ID can be a number like `1` or something complex like a `[1 {:region :us}]`. You can attach multiple 73 | IDs for same tag. 74 | 75 | You can add the tag ID pair inside the cached function or in the ret-fn: 76 | 77 | ```clojure 78 | (defn get-person-by-id [db-conn account-id person-id] 79 | (if (nil? person-id) 80 | {:status 404} 81 | (-> {:status 200} 82 | (m/with-tag-id :person person-id) 83 | (m/with-tag-id :account account-id)))) 84 | 85 | (m/memo #'get-person-by-id [:person :account] cache/inf-cache) 86 | ``` 87 | 88 | Now you can invalidate all entries linked to a specified ID in any correctly tagged cache: 89 | 90 | ```clojure 91 | (m/memo-clear-tag! :account 1) 92 | ``` 93 | 94 | This will invalidate entries with tag id `:account, 1` in all `:account` tagged functions. 95 | 96 | As mentioned, you can move code that adds the id information to a `ret-fn`: 97 | 98 | ```clojure 99 | ; first argument is args, second is the returned value 100 | (defn ret-fn [[_ account-id person-id :as args] resp] 101 | (if (<= 400 (:status resp) 599) 102 | (m/do-not-cache resp) 103 | (-> resp 104 | ; we can grab the data from arg list 105 | (m/with-tag-id :account account-id) 106 | (m/with-tag-id :person person-id) 107 | ; or we can grab it from the return value 108 | (m/with-tag-id :person (:id resp))))) 109 | 110 | (defn get-person-by-id [db-conn account-id person-id] 111 | (if (nil? person-id) 112 | {:status 404} 113 | {:status 200 :id person-id :name ....})) 114 | 115 | (m/memo #'get-person-by-id [:person :account] (assoc cache/inf-cache mc/ret-fn ret-fn)) 116 | ``` 117 | 118 | Later you can invalidate tagged entries: 119 | 120 | ```clojure 121 | (m/memo-clear-tag! :person 1) 122 | 123 | ;; get better atomicity with bulk operation 124 | 125 | (m/memo-clear-tags! [:person 1] [:user 33]) 126 | ``` 127 | 128 | ## Invalidation atomicity 129 | 130 | ``` 131 | 132 | ``` 133 | -------------------------------------------------------------------------------- /java/memento/base/LockoutMap.java: -------------------------------------------------------------------------------- 1 | package memento.base; 2 | 3 | import clojure.lang.IPersistentSet; 4 | import clojure.lang.ISeq; 5 | import clojure.lang.ITransientMap; 6 | import clojure.lang.PersistentHashMap; 7 | 8 | import java.util.concurrent.CopyOnWriteArraySet; 9 | import java.util.concurrent.atomic.AtomicReference; 10 | 11 | /** 12 | * This class represents a global map of ongoing bulk invalidations of Tag Ids. Await lockout can be used 13 | * to await for bulk invalidation to finish. Adding listeners is used to enable implementations to 14 | * be able to communicate these lockouts outside the JVM. 15 | */ 16 | public class LockoutMap { 17 | 18 | public static LockoutMap INSTANCE = new LockoutMap(); 19 | 20 | private final AtomicReference m = new AtomicReference<>(PersistentHashMap.EMPTY); 21 | 22 | public LockoutMap() { 23 | 24 | } 25 | 26 | private final CopyOnWriteArraySet listeners = new CopyOnWriteArraySet<>(); 27 | 28 | public void addListener(Listener l) { 29 | listeners.add(l); 30 | } 31 | 32 | /** 33 | * Add a new lockout, returning the Latch, (if newer than existing) for the given keys 34 | * 35 | * @param tagsAndIds 36 | * @return 37 | */ 38 | public void startLockout(Iterable tagsAndIds, LockoutTag tag) { 39 | PersistentHashMap oldMap; 40 | PersistentHashMap newv; 41 | do { 42 | oldMap = m.get(); 43 | ITransientMap newMap = oldMap.asTransient(); 44 | for (Object e : tagsAndIds) { 45 | newMap.assoc(e, tag); 46 | } 47 | newv = (PersistentHashMap) newMap.persistent(); 48 | } while (!m.compareAndSet(oldMap, newv)); 49 | listeners.forEach(l -> l.startLockout(tagsAndIds, tag)); 50 | } 51 | 52 | /** 53 | * End lockout for keys and the marker. After map is updated, marker's latch is released 54 | * 55 | * @param tagsAndIds 56 | * @param tag 57 | */ 58 | public void endLockout(Iterable tagsAndIds, LockoutTag tag) { 59 | PersistentHashMap oldMap; 60 | PersistentHashMap newv; 61 | do { 62 | oldMap = m.get(); 63 | ITransientMap newMap = oldMap.asTransient(); 64 | for (Object e : tagsAndIds) { 65 | if (oldMap.get(e) == tag) { 66 | newMap.without(e); 67 | } 68 | } 69 | newv = (PersistentHashMap) newMap.persistent(); 70 | } while (!m.compareAndSet(oldMap, newv)); 71 | try { 72 | listeners.forEach(l -> l.endLockout(tagsAndIds, tag)); 73 | } finally { 74 | tag.getLatch().countDown(); 75 | } 76 | } 77 | 78 | private static boolean awaitMarker(PersistentHashMap lockouts, Object obj) throws InterruptedException { 79 | LockoutTag lockoutTag = (LockoutTag) lockouts.get(obj); 80 | if (lockoutTag != null) { 81 | lockoutTag.getLatch().await(); 82 | return true; 83 | } else { 84 | return false; 85 | } 86 | } 87 | 88 | /** 89 | * It awaits an invalidations to finish, returns after that. Returns true if the entry 90 | * was invalid and an invalidation was awaited. 91 | */ 92 | public static boolean awaitLockout(Object promiseValue) throws InterruptedException { 93 | if (promiseValue instanceof EntryMeta) { 94 | IPersistentSet idents = ((EntryMeta) promiseValue).getTagIdents(); 95 | if (idents.count() != 0) { 96 | PersistentHashMap invalidations = LockoutMap.INSTANCE.m.get(); 97 | if (invalidations.isEmpty()) { 98 | return false; 99 | } 100 | ISeq identSeq = ((EntryMeta) promiseValue).getTagIdents().seq(); 101 | boolean ret = false; 102 | while (identSeq != null) { 103 | ret |= awaitMarker(invalidations, identSeq.first()); 104 | identSeq = identSeq.next(); 105 | } 106 | return ret; 107 | } 108 | } 109 | return false; 110 | } 111 | 112 | public interface Listener { 113 | void startLockout(Iterable tagsAndIds, LockoutTag latch); 114 | 115 | void endLockout(Iterable tagsAndIds, LockoutTag latch); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/memento/caffeine/config.clj: -------------------------------------------------------------------------------- 1 | (ns memento.caffeine.config 2 | "Caffeine implementation config helpers. 3 | 4 | Contains documented definitions of standard options of Caffeine cache config." 5 | {:author "Rok Lenarčič"} 6 | (:import (memento.caffeine Expiry))) 7 | 8 | (def removal-listener 9 | "Cache setting, corresponds to .removalListener on Caffeine. 10 | 11 | A function of four arguments (fn [f key value removal-cause] nil), 12 | that will be called whenever an entry is removed. 13 | 14 | The four arguments are: 15 | 16 | - the function being cached 17 | - the key (arg-list transformed by key-fn if any) 18 | - the value (after ret-fn being applied) 19 | - com.github.benmanes.caffeine.cache.RemovalCause 20 | 21 | Warning: any exception thrown by listener will not be propagated to the Cache user, only logged via a Logger." 22 | :memento.caffeine/removal-listener) 23 | 24 | (def weight< 25 | "Cache setting, a long. 26 | 27 | Specifies the maximum weight of entries the cache may contain. If using this option, 28 | you must provide `kw-weight` option for cache to calculate the weight of entries. 29 | 30 | A cache may evict entries before the specified limit is reached." 31 | :memento.caffeine/weight<) 32 | 33 | (def kv-weight 34 | "Cache setting, a function of 3 arguments (fn [f key value] int-weight), 35 | that will be used to determine the weight of entries. 36 | 37 | It should return an int, the weight of the entry. 38 | 39 | The 3 arguments are: 40 | - the first argument is the function being cached 41 | - the second argument is the key (arg-list transformed by key-fn if any) 42 | - the third argument is the value (after ret-fn being applied)" 43 | :memento.caffeine/kv-weight) 44 | 45 | ;; makes no sense, since user cannot hold on to our CacheKey instances 46 | #_(def weak-keys 47 | "Cache setting, corresponds to .weakKeys on CacheBuilder. 48 | 49 | Boolean flag, enabling storing keys using weak references. 50 | 51 | Specifies that each key (not value) stored in the cache should be wrapped in a WeakReference 52 | (by default, strong references are used). 53 | 54 | Warning: when this method is used, the resulting cache will use identity (==) comparison 55 | to determine equality of keys. Its Cache.asMap() view will therefore technically violate 56 | the Map specification (in the same way that IdentityHashMap does). 57 | 58 | The identity comparison makes this not very useful." 59 | :memento.caffeine/weak-keys) 60 | 61 | (def weak-values 62 | "Cache setting, corresponds to .weakValues on CacheBuilder. 63 | 64 | Boolean flag, enabling storing values using weak references. 65 | 66 | This allows entries to be garbage-collected if there are no other (strong or soft) references to the values." 67 | :memento.caffeine/weak-values) 68 | 69 | (def soft-values 70 | "Cache setting, corresponds to .softValues on CacheBuilder. 71 | 72 | Boolean flag, enabling storing values using soft references. 73 | 74 | Softly referenced objects are garbage-collected in a globally least-recently-used manner, 75 | in response to memory demand. Because of the performance implications of using soft references, 76 | we generally recommend using the more predictable maximum cache size instead." 77 | :memento.caffeine/soft-values) 78 | 79 | (def stats 80 | "Cache setting, boolean flag, enabling collection of stats. 81 | 82 | Corresponds to .enableStats on CacheBuilder. 83 | 84 | You can retrieve a cache's stats by using memento.caffeine/stats. 85 | 86 | Returns com.google.common.cache.CacheStats instance or nil." 87 | :memento.caffeine/stats) 88 | 89 | (def ticker 90 | "Cache setting, corresponds to .ticker on CacheBuilder. 91 | 92 | A function of zero arguments that should return current nano time. 93 | This is used when doing time based eviction. 94 | 95 | The default is (fn [] (System/nanoTime)). 96 | 97 | This is useful for testing and you can also make the time move in discrete amounts (e.g. you can 98 | make all cache accesses in a request have same time w.r.t. eviction)." 99 | :memento.caffeine/ticker) 100 | 101 | (def expiry 102 | "A cache and function bind setting, an instance of memento.caffeine.Expiry interface. 103 | 104 | Enables user to specify cached entry expiry on each entry individually. If interface 105 | functions return nil, then ttl and fade settings apply." 106 | :memento.caffeine/expiry) 107 | 108 | (def meta-expiry 109 | "A memento.caffeine.Expiry instance that looks for fade and ttl keys on object metas and uses those to control 110 | variable expiry." 111 | Expiry/META_VAL_EXP) -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.68 4 | 5 | - correctly wraps MultiFn 6 | 7 | ## 2.0.65 8 | 9 | - added a check that will throw an error if encountering mc/cache key in wrong place, cache configuration 10 | 11 | ## 2.0.63 12 | 13 | - upgrade from Caffeine 2 to Caffeine 3. Min Java changed from 8 to 11. 14 | 15 | ## 1.4.62 16 | 17 | - remove unneeded operation when adding entries into the map 18 | 19 | ## 1.4.61 20 | 21 | - when an tag is invalidated during load, the loading thread will be interrupted 22 | 23 | ## 1.3.60 24 | 25 | - improved variable expiry 26 | - added stronger prevention of the use of invalid entries 27 | 28 | ## 1.2.59 29 | 30 | - added variable expiry option (see README) 31 | - removed some reflection 32 | - enabled weakValues as an option for caffeine cache 33 | - removed weakKeys as it's not possible to use that option 34 | 35 | ## 1.2.58 36 | 37 | - added ret-ex-fn option to transform exceptions being thrown by cache in the same way ret-fn works for values 38 | 39 | ## 1.2.57 40 | 41 | - add predicate memento.core/none-cache? that checks if cache is of none type 42 | - new function memento.core/caches-by-tag 43 | - important improvement of atomicity for invalidations by tag or function 44 | - important fix for thread synchronization when adding tagged entries 45 | - important fix for secondary indexes clearing 46 | - reduced memory use 47 | - improving performance on evictions when an eviction listener isn't used 48 | - *BREAKING CHANGE FOR IMPLEMENTATIONS* `invalidateId` is now `invalidateIds` and takes an iterable of tag ids, the implementations are expected to take care to block loads until invalidations are complete. Use the `memento.base/lockout-map` for this purpose. 49 | 50 | ## 1.1.54 51 | 52 | - Improve handling of Vectors when adding entries 53 | 54 | ## 1.1.53 55 | 56 | - improve memory use 57 | 58 | ## 1.1.52 59 | 60 | - fixes a bug with concurrent loads causing some of them to return nil as a result 61 | 62 | ## 1.1.51 63 | 64 | - DO NOT USE 65 | - add check for cyclical loads to caffeine cache, e.g. cached function calling itself with same parameters, this now throws StackOverflowError, which is the error you'd get in this situation with uncached function 66 | - improved performance 67 | 68 | ## 1.1.50 69 | 70 | - added option of using SoftReferences in caffeine cache 71 | - fixed reload-guards? var not being redefinable 72 | 73 | ## 1.1.45 74 | 75 | - add getters/setters to MultiCache for the delegate/upstream cache, also add clojure functions to access these properties to `memento.multi` namespace 76 | - moved CacheKey to memento.base Java package 77 | 78 | ## 1.1.44 79 | 80 | - fix bug which would have the cache return nil when concurrently accessing a value being calculated that ends being uncacheable 81 | 82 | ## 1.1.42 83 | - big internal changes now uses Java objects for most things for smaller memory profile and smaller callstack 84 | - significant improvements to callstack size for cached call 85 | - **This is breaking changes for any implementation, shouldn't affect users** 86 | - Fixes issue where namespace scan would stack caches on the same function over and over if called multiple times 87 | 88 | Here is an example of previous callstack of recursively cached call (without ret-fn): 89 | ```clojure 90 | at myns$myfn.invoke(myns.clj:12) 91 | at clojure.lang.AFn.applyToHelper(AFn.java:160) 92 | at clojure.lang.AFn.applyTo(AFn.java:144) 93 | at clojure.core$apply.invokeStatic(core.clj:667) 94 | at clojure.core$apply.invoke(core.clj:662) 95 | at memento.caffeine.CaffeineCache$fn__2536.invoke(caffeine.clj:122) 96 | at memento.caffeine.CaffeineCache.cached(caffeine.clj:121) 97 | at memento.mount.UntaggedMountPoint.cached(mount.clj:50) 98 | at memento.mount$bind$fn__2432.doInvoke(mount.clj:119) 99 | at clojure.lang.RestFn.applyTo(RestFn.java:137) 100 | at clojure.lang.AFunction$1.doInvoke(AFunction.java:31) 101 | at clojure.lang.RestFn.invoke(RestFn.java:436) 102 | at myns$myfn.invokeStatic(myns.clj:17) 103 | at myns$myfn.invoke(myns.clj:12) 104 | ``` 105 | 106 | And callstack after: 107 | ```clojure 108 | at myns$myfn.invoke(myns.clj:12) 109 | at clojure.lang.AFn.applyToHelper(AFn.java:160) 110 | at memento.caffeine.CaffeineCache$fn__2052.invoke(caffeine.clj:120) 111 | at memento.caffeine.CaffeineCache.cached(caffeine.clj:119) 112 | at memento.mount.CachedFn.invoke(CachedFn.java:110) 113 | at myns$myfn.invokeStatic(myns.clj:17) 114 | at myns$myfn.invoke(myns.clj:12) 115 | ``` 116 | 117 | From 11 stack frames to 4. 118 | 119 | ## 1.0.37 120 | - remove Guava and replace with Caffeine 121 | - rewrite the readme 122 | - mark Guava namespaces for deprecation 123 | 124 | ## 0.9.36 (2022-03-01) 125 | - add `memento.core/defmemo` 126 | 127 | ## 0.9.35 (2022-02-22) 128 | - add `memento.core/key-fn*` mount setting 129 | 130 | ## 0.9.34 (2022-02-16) 131 | - add support for using meta 132 | 133 | ## 0.9.3 (2021-05-08) 134 | ### Features added 135 | - `memento.core/if-config` 136 | -------------------------------------------------------------------------------- /java/memento/caffeine/SpecialPromise.java: -------------------------------------------------------------------------------- 1 | package memento.caffeine; 2 | 3 | import clojure.lang.ISeq; 4 | import memento.base.EntryMeta; 5 | import memento.base.LockoutMap; 6 | 7 | import java.util.HashSet; 8 | import java.util.concurrent.CountDownLatch; 9 | 10 | /** 11 | * Special promise for use in indirection in Caffeine cache. Do not use otherwise. 12 | * 13 | * This class is NOT threadsafe. The intended use is as a faster CompletableFuture that is 14 | * aware of the thread that made it, so it can detect when same thread is trying to await on it 15 | * and throw to prevent deadlock. 16 | * 17 | * It is also expected that it is created and delivered by the same thread and it is expected that 18 | * any other thread is awaiting result and that only a single attempt is made at delivering the result. 19 | * 20 | * It does not include logic to deal with multiple calls to deliver the promise, as it's optimized for 21 | * a specific use case. 22 | */ 23 | public class SpecialPromise { 24 | 25 | private static final AltResult NIL = new AltResult(null); 26 | private final CountDownLatch d = new CountDownLatch(1); 27 | // these 2 don't need to be thread-safe, because they are only used to check 28 | // if current thread is one that created and started the load on the promise 29 | // so even with non-volatile, check is only true if thread is same as current thread 30 | // so no memory barrier needed 31 | private final HashSet invalidatedIds = new HashSet<>(); 32 | private volatile Thread thread; 33 | private volatile Object result; 34 | 35 | public void init() { 36 | this.thread = Thread.currentThread(); 37 | } 38 | 39 | public Object await(Object stackOverflowContext) throws Throwable { 40 | if (thread == Thread.currentThread()) { 41 | throw new StackOverflowError("Recursive load on key: " + stackOverflowContext); 42 | } 43 | Object r; 44 | if ((r = result) == null) { 45 | d.await(); 46 | r = result; 47 | } 48 | if (r instanceof AltResult) { 49 | Throwable x = ((AltResult) r).value; 50 | if (x == null) { 51 | return null; 52 | } else { 53 | throw x; 54 | } 55 | } else { 56 | return r; 57 | } 58 | } 59 | 60 | private boolean isLockedOut(EntryMeta em) { 61 | try { 62 | return LockoutMap.awaitLockout(em); 63 | } catch (InterruptedException e) { 64 | return true; 65 | } 66 | } 67 | 68 | // Returns true if delivered object is viable 69 | public boolean deliver(Object r) { 70 | if (r instanceof EntryMeta) { 71 | EntryMeta em = (EntryMeta) r; 72 | if (isLockedOut(em) || hasInvalidatedTagId(em)) { 73 | result = EntryMeta.absent; 74 | return false; 75 | } 76 | } 77 | if (result != EntryMeta.absent) { 78 | result = r == null ? NIL : r; 79 | return true; 80 | } 81 | return false; 82 | } 83 | 84 | public void deliverException(Throwable t) { 85 | result = new AltResult(t); 86 | } 87 | 88 | public Object getNow() throws Throwable { 89 | Object r; 90 | if (d.getCount() != 0) { 91 | return EntryMeta.absent; 92 | } 93 | if ((r = result) instanceof AltResult) { 94 | Throwable x = ((AltResult) r).value; 95 | if (x == null) { 96 | return null; 97 | } else { 98 | throw x; 99 | } 100 | } else { 101 | return r == null ? EntryMeta.absent : r; 102 | } 103 | } 104 | 105 | public void invalidate() { 106 | result = EntryMeta.absent; 107 | thread.interrupt(); 108 | } 109 | 110 | public boolean isInvalid() { 111 | return result == EntryMeta.absent; 112 | } 113 | 114 | public void releaseResult() { 115 | d.countDown(); 116 | } 117 | 118 | private boolean hasInvalidatedTagId(EntryMeta entryMeta) { 119 | synchronized (invalidatedIds) { 120 | ISeq s = entryMeta.getTagIdents().seq(); 121 | while (s != null) { 122 | if (invalidatedIds.contains(s.first())) { 123 | return true; 124 | } 125 | s = s.next(); 126 | } 127 | } 128 | return false; 129 | } 130 | 131 | public void addInvalidIds(Iterable ids) { 132 | synchronized (invalidatedIds) { 133 | for (Object id : ids) { 134 | invalidatedIds.add(id); 135 | } 136 | } 137 | } 138 | 139 | private static class AltResult { 140 | Throwable value; 141 | 142 | public AltResult(Throwable value) { 143 | this.value = value; 144 | } 145 | } 146 | 147 | } -------------------------------------------------------------------------------- /src/memento/caffeine.clj: -------------------------------------------------------------------------------- 1 | (ns memento.caffeine 2 | "Caffeine cache implementation." 3 | {:author "Rok Lenarčič"} 4 | (:require [memento.base :as b]) 5 | (:import (java.util.concurrent TimeUnit) 6 | (memento.base Durations CacheKey EntryMeta ICache Segment) 7 | (com.github.benmanes.caffeine.cache Caffeine Weigher Ticker) 8 | (memento.caffeine CaffeineCache_ SecondaryIndex SpecialPromise Expiry) 9 | (memento.mount IMountPoint))) 10 | 11 | (defn create-expiry 12 | "Assumes variable expiry is needed. So either ttl or fade is a function." 13 | [ttl fade ^Expiry cache-expiry] 14 | (let [read-default (some-> (or ttl fade) (Durations/nanos)) 15 | write-default (Durations/nanos (or ttl fade [Long/MAX_VALUE :ns]))] 16 | (reify com.github.benmanes.caffeine.cache.Expiry 17 | (expireAfterCreate [this k v current-time] 18 | (if (instance? SpecialPromise v) 19 | Long/MAX_VALUE 20 | (.expireAfterUpdate this k v current-time Long/MAX_VALUE))) 21 | (expireAfterUpdate [this k v current-time current-duration] 22 | (if-let [ret (.ttl cache-expiry {} (.getArgs ^CacheKey k) v)] 23 | (Durations/nanos ret) 24 | (if-let [ret (.fade cache-expiry {} (.getArgs ^CacheKey k) v)] 25 | (Durations/nanos ret) 26 | write-default))) 27 | (expireAfterRead [this k v current-time current-duration] 28 | (if (instance? SpecialPromise v) 29 | current-duration 30 | ;; if fade is not specified, keep current validity (probably set by ttl) 31 | (if-let [ret (.fade cache-expiry {} (.getArgs ^CacheKey k) v)] 32 | (Durations/nanos ret) 33 | (or read-default current-duration))))))) 34 | 35 | (defn conf->sec-index 36 | "Creates secondary index for evictions" 37 | [{:memento.core/keys [concurrency]}] 38 | (SecondaryIndex. (or concurrency 4))) 39 | 40 | (defn ^Caffeine conf->builder 41 | "Creates and configures common parameters on the builder." 42 | [{:memento.core/keys [initial-capacity size< ttl fade] 43 | :memento.caffeine/keys [weight< removal-listener kv-weight weak-keys weak-values 44 | soft-values refresh stats ticker expiry]}] 45 | (cond-> (Caffeine/newBuilder) 46 | removal-listener (.removalListener (CaffeineCache_/listener removal-listener)) 47 | initial-capacity (.initialCapacity initial-capacity) 48 | weight< (.maximumWeight weight<) 49 | size< (.maximumSize size<) 50 | kv-weight (.weigher 51 | (reify Weigher (weigh [_this k v] 52 | (kv-weight (.getId ^CacheKey k) 53 | (.getArgs ^CacheKey k) 54 | (b/unwrap-meta v))))) 55 | ;; these don't make sense as the caller cannot hold the CacheKey 56 | ;;weak-keys (.weakKeys) 57 | ;; careful around EntryMeta objects 58 | ;; mean that cached values have another wrapper yet again 59 | weak-values (.weakValues) 60 | soft-values (.softValues) 61 | expiry (.expireAfter (create-expiry ttl fade expiry)) 62 | (and (not expiry) ttl) (.expireAfterWrite (Durations/nanos ttl) TimeUnit/NANOSECONDS) 63 | (and (not expiry) fade) (.expireAfterAccess (Durations/nanos fade) TimeUnit/NANOSECONDS) 64 | ;; not currently used because we don't build a loading cache 65 | refresh (.refreshAfterWrite (Durations/nanos refresh) TimeUnit/NANOSECONDS) 66 | ticker (.ticker (proxy [Ticker] [] (read [] (ticker)))) 67 | stats (.recordStats))) 68 | 69 | (defn assoc-imm-val! 70 | "If cached value is a completable future with immediately available value, assoc it to transient." 71 | [transient-m k v xf] 72 | (let [cv (if (instance? SpecialPromise v) 73 | (.getNow ^SpecialPromise v) 74 | v)] 75 | (if (identical? cv b/absent) 76 | transient-m 77 | (assoc! transient-m k (xf cv))))) 78 | 79 | ;;;;;;;;;;;;;; 80 | ; ->key-fn take 2 args f and key 81 | (defrecord CaffeineCache [conf ^CaffeineCache_ caffeine-cache] 82 | ICache 83 | (conf [this] conf) 84 | (cached [this segment args] 85 | (.cached caffeine-cache segment args)) 86 | (ifCached [this segment args] 87 | (.ifCached caffeine-cache segment args)) 88 | (invalidate [this segment] 89 | (.invalidate caffeine-cache ^Segment segment) 90 | this) 91 | (invalidate [this segment args] (.invalidate caffeine-cache ^Segment segment args) 92 | this) 93 | (invalidateAll [this] (.invalidateAll caffeine-cache) this) 94 | (invalidateIds [this ids] 95 | (.invalidateIds caffeine-cache ids) 96 | this) 97 | (addEntries [this segment args-to-vals] 98 | (.addEntries caffeine-cache segment args-to-vals) 99 | this) 100 | (asMap [this] (persistent! 101 | (reduce (fn [m [k v]] (assoc-imm-val! m k v b/unwrap-meta)) 102 | (transient {}) 103 | (.asMap caffeine-cache)))) 104 | (asMap [this segment] 105 | (persistent! 106 | (reduce (fn [m [^CacheKey k v]] 107 | (if (= (.getId segment) (.getId k)) (assoc-imm-val! m (.getArgs k) v b/unwrap-meta) 108 | m)) 109 | (transient {}) 110 | (.asMap caffeine-cache))))) 111 | 112 | (defmethod b/new-cache :memento.core/caffeine [conf] 113 | (->CaffeineCache conf (CaffeineCache_. 114 | (conf->builder conf) 115 | (:memento.core/key-fn conf) 116 | (:memento.core/ret-fn conf) 117 | (:memento.core/ret-ex-fn conf) 118 | (conf->sec-index conf)))) 119 | 120 | (defn stats 121 | "Return caffeine stats for the cache if it is a caffeine Cache. 122 | 123 | Takes a memoized fn or a Cache instance as a parameter. 124 | 125 | Returns com.github.benmanes.caffeine.cache.stats.CacheStats" 126 | [fn-or-cache] 127 | (if (instance? ICache fn-or-cache) 128 | (when (instance? CaffeineCache fn-or-cache) 129 | (.stats ^CaffeineCache_ (:caffeine-cache fn-or-cache))) 130 | (stats (.mountedCache ^IMountPoint fn-or-cache)))) 131 | 132 | (defn to-data [cache] 133 | (when-let [caffeine (:caffeine-cache cache)] 134 | (persistent! 135 | (reduce (fn [m [^CacheKey k v]] (assoc-imm-val! m 136 | [(.getId k) (.getArgs k)] 137 | v 138 | #(if (and (instance? EntryMeta %) (nil? (.getV ^EntryMeta %))) 139 | nil %))) 140 | (transient {}) 141 | (.asMap ^CaffeineCache_ caffeine))))) 142 | 143 | (defn load-data [cache data-map] 144 | (.loadData ^CaffeineCache_ (:caffeine-cache cache) data-map) 145 | cache) 146 | -------------------------------------------------------------------------------- /src/memento/mount.clj: -------------------------------------------------------------------------------- 1 | (ns memento.mount 2 | "Mount points, they serve as glue between a cache that can house entries from 3 | multiple functions and the individual functions." 4 | {:author "Rok Lenarčič"} 5 | (:require [memento.base :as base] 6 | [memento.config :as config]) 7 | (:import (clojure.lang AFn ISeq MultiFn) 8 | (memento.base ICache Segment) 9 | (memento.mount Cached CachedFn CachedMultiFn IMountPoint))) 10 | 11 | (def ^:dynamic *caches* "Contains map of mount point to cache instance" {}) 12 | (def tags "Map tag to mount-point" (atom {})) 13 | 14 | (def configuration-props [config/key-fn config/ret-fn config/seed config/tags 15 | config/evt-fn config/id config/key-fn* config/ret-ex-fn]) 16 | 17 | (defn assoc-cache-tags 18 | "Add Mount Point ref to tag index" 19 | [index cache-tags ref] 20 | (reduce #(update %1 %2 (fnil conj #{}) ref) index cache-tags)) 21 | 22 | (defn dissoc-cache-tags 23 | "Remove Mount Point ref from tag index" 24 | [index ref] 25 | (reduce-kv #(assoc %1 %2 (disj %3 ref)) {} index)) 26 | 27 | (deftype TagsUnloader [cache-mount] 28 | Runnable 29 | (run [this] 30 | (swap! tags dissoc-cache-tags cache-mount) 31 | (alter-var-root #'*caches* dissoc cache-mount) 32 | nil)) 33 | 34 | (defrecord UntaggedMountPoint [^ICache cache ^Segment segment evt-handler] 35 | IMountPoint 36 | (asMap [this] (.asMap cache segment)) 37 | (cached [this args] (.cached cache segment args)) 38 | (ifCached [this args] (.ifCached cache segment args)) 39 | (getTags [this] []) 40 | (handleEvent [this evt] (evt-handler this evt)) 41 | (invalidate [this args] (.invalidate cache segment args)) 42 | (invalidateAll [this] (.invalidate cache segment)) 43 | (mountedCache [this] cache) 44 | (addEntries [this args-to-vals] (.addEntries cache segment args-to-vals)) 45 | (segment [this] segment)) 46 | 47 | (defrecord TaggedMountPoint [tags ^Segment segment evt-handler] 48 | IMountPoint 49 | (asMap [this] (.asMap ^ICache (*caches* this base/no-cache) segment)) 50 | (cached [this args] (.cached ^ICache (*caches* this base/no-cache) segment args)) 51 | (ifCached [this args] (.ifCached ^ICache (*caches* this base/no-cache) segment args)) 52 | (getTags [this] tags) 53 | (handleEvent [this evt] (evt-handler this evt)) 54 | (invalidate [this args] (.invalidate ^ICache (*caches* this base/no-cache) segment args)) 55 | (invalidateAll [this] (.invalidate ^ICache (*caches* this base/no-cache) segment)) 56 | (mountedCache [this] (*caches* this base/no-cache)) 57 | (addEntries [this args-to-vals] 58 | (.addEntries ^ICache (*caches* this base/no-cache) segment args-to-vals)) 59 | (segment [this] segment)) 60 | 61 | (defn mounted-cache [^IMountPoint mp] (.mountedCache mp)) 62 | 63 | (defn reify-mount-conf 64 | "Transform user given mount-conf to a canonical form of a map." 65 | [mount-conf] 66 | (if (map? mount-conf) 67 | mount-conf 68 | {config/tags ((if (sequential? mount-conf) vec vector) mount-conf)})) 69 | 70 | (defn wrap-fn 71 | [f ret-fn ret-ex-fn] 72 | (cond 73 | (and ret-fn ret-ex-fn) (fn [& args] 74 | (try (ret-fn args (AFn/applyToHelper f args)) 75 | (catch Throwable t (throw (ret-ex-fn args t))))) 76 | ret-fn (fn [& args] (ret-fn args (AFn/applyToHelper f args))) 77 | ret-ex-fn (fn [& args] 78 | (try (AFn/applyToHelper f args) 79 | (catch Throwable t (throw (ret-ex-fn args t))))) 80 | :else f)) 81 | 82 | (defn create-mount 83 | "Create mount record by specified map conf" 84 | [f cache mount-conf] 85 | (let [key-fn (or (config/key-fn mount-conf) 86 | (when-let [base (config/key-fn* mount-conf)] 87 | (fn [args] (AFn/applyToHelper base (if (instance? ISeq args) args (seq args))))) 88 | identity) 89 | evt-fn (config/evt-fn mount-conf (fn [_ _] nil)) 90 | f* (wrap-fn f (config/ret-fn mount-conf) (config/ret-ex-fn mount-conf)) 91 | segment (Segment. f* key-fn (mount-conf config/id f) mount-conf)] 92 | (if-let [t (config/tags mount-conf)] 93 | (let [wrapped-t (if (sequential? t) t (vector t)) 94 | mp (->TaggedMountPoint wrapped-t segment evt-fn)] 95 | (alter-var-root #'*caches* assoc mp cache) 96 | (swap! tags assoc-cache-tags wrapped-t mp) 97 | mp) 98 | (->UntaggedMountPoint cache segment evt-fn)))) 99 | 100 | (defn bind 101 | "Bind a cache to a fn or var. Internal function." 102 | [fn-or-var mount-conf cache] 103 | (if (var? fn-or-var) 104 | (let [mount-conf (-> mount-conf 105 | reify-mount-conf 106 | (update config/id #(or % (.intern (str fn-or-var)))))] 107 | (alter-var-root fn-or-var bind mount-conf cache)) 108 | (let [mount-conf (reify-mount-conf mount-conf) 109 | constructor (if (instance? MultiFn fn-or-var) 110 | #(CachedMultiFn. (str (config/id mount-conf)) %1 %2 %3 %4) 111 | #(CachedFn. %1 %2 %3 %4)) 112 | stacking (if (instance? Cached fn-or-var) (config/bind-mode mount-conf :new) :none) 113 | ^IMountPoint cache-mount (case stacking 114 | :new (create-mount (.getOriginalFn ^Cached fn-or-var) cache mount-conf) 115 | :keep (.getMp ^Cached fn-or-var) 116 | (:none :stack) (create-mount fn-or-var cache mount-conf)) 117 | reload-guard (when (and config/reload-guards? (config/tags mount-conf) (not= :keep stacking)) 118 | (doto (Object.) 119 | (IMountPoint/register (->TagsUnloader cache-mount)))) 120 | f (case stacking 121 | :keep fn-or-var 122 | (:new :stack) (constructor reload-guard cache-mount (meta fn-or-var) (.getOriginalFn ^Cached fn-or-var)) 123 | :none (constructor reload-guard cache-mount (meta fn-or-var) fn-or-var))] 124 | (.addEntries (.getMp ^Cached f) (config/seed mount-conf {})) 125 | f))) 126 | 127 | (defn mount-point 128 | "Return active mount point from the object's meta." 129 | [obj] 130 | (when (instance? IMountPoint obj) obj)) 131 | 132 | (defn update-existing 133 | "Convenience function. Updates ks's that are present with the provided update fn." 134 | [m ks update-fn] 135 | (reduce #(if-let [kv (find %1 %2)] (assoc %1 %2 (update-fn (val kv))) %1) m ks)) 136 | 137 | (defn alter-caches-mapping 138 | "Internal function. Modifies entire tagged cache map with the provided function. 139 | Applies the function as (fn [*caches* refs & other-update-fn-args])" 140 | [tag update-fn & update-fn-args] 141 | (let [refs (get @tags tag []) 142 | update-fn #(apply update-fn % refs update-fn-args)] 143 | (if (.getThreadBinding #'*caches*) 144 | (var-set #'*caches* (update-fn *caches*)) 145 | (alter-var-root #'*caches* update-fn)))) 146 | -------------------------------------------------------------------------------- /src/memento/config.clj: -------------------------------------------------------------------------------- 1 | (ns memento.config 2 | "Memoization library config. 3 | 4 | Contains global settings that manipulate the cache mechanisms. 5 | See doc strings. 6 | 7 | Contains conf settings as vars. 8 | 9 | Also contains documented definitions of standard options of cache config." 10 | {:author "Rok Lenarčič"} 11 | (:refer-clojure :exclude [type]) 12 | (:import (java.util.concurrent TimeUnit))) 13 | 14 | (def ^:redef enabled? 15 | "If false, then all cache attach operations create a cache that does no 16 | caching (changing this value doesn't affect caches already created). 17 | 18 | Initially has the value of java property `memento.enabled` (defaulting to true)." 19 | (Boolean/valueOf (System/getProperty "memento.enabled" "true"))) 20 | 21 | (def ^:redef reload-guards? 22 | "If true, then whenever a function cached with tags is garbage collected (e.g. after a namespace reload in REPL), 23 | a cleanup is done of global tags map. Can be turned off if you don't intend to reload namespaces or do other 24 | actions that would GC cached function instances. 25 | 26 | Initially has the value of java property `memento.reloadable` (defaulting to true)." 27 | (Boolean/valueOf (System/getProperty "memento.reloadable" "true"))) 28 | 29 | (def ^:dynamic *default-type* "Default cache type." :memento.core/none) 30 | 31 | (def type 32 | "Cache setting, type of cache or region that will be instantiated, a keyword. 33 | 34 | The library has two built-ins: 35 | - memento.core/none 36 | - memento.core/caffeine 37 | 38 | If not specified the caches created default to *default-type*." 39 | :memento.core/type) 40 | 41 | (def bind-mode 42 | "Function bind setting, defaults to :new. It governs what the bind will do if you try to bind 43 | a cache to a function that is already cached, e.g. what happens when memo is called multiple times 44 | on same Var. Options are: 45 | - :keep, keeps old cache binding 46 | - :new, keeps the new cache binding 47 | - :stack, stacks the caches, so the new binding wraps the older cached function" 48 | :memento.core/bind-mode) 49 | 50 | (def key-fn 51 | "Cache and function bind setting, a function to be used to calculate the cache key (fn [f-args] key). 52 | 53 | Cache key affects what is considered the 'same' argument list for a function and it will affect caching in that manner. 54 | 55 | If both function bind and cache have this setting, then function bind key-fn is applied first. 56 | 57 | It's a function of 1 argument, the seq of function arguments. If not provided it defaults to identity." 58 | :memento.core/key-fn) 59 | 60 | (def key-fn* 61 | "Function bind setting, works same as key-fn but the provided function will receive all 62 | arguments that the original function does. If both key-fn and key-fn* are provided, key-fn is used." 63 | :memento.core/key-fn*) 64 | 65 | (def ret-fn 66 | "Cache and function bind setting, a function that is ran to process the return of the function, before it's memoized, 67 | (fn [fn-args ret-value] transformed-value). 68 | 69 | It can provide some generic transformation facility, but more importantly, it can wrap specific return 70 | values in 'do-not-cache' object, that prevents caching or wrap with tagged IDs." 71 | :memento.core/ret-fn) 72 | 73 | (def evt-fn 74 | "Function bind setting, a function that is invoked when any event is fired at the function. 75 | 76 | (fn [mnt-point event] void) 77 | 78 | Useful generally to push data into the related cache, the mnt-point parameter implement MountPoint protocol 79 | so you can invoke memo-add! and such on it. The event can be any data, it's probably best to come up with 80 | a format that enables the functions that receive the event to be able to tell them apart." 81 | :memento.core/evt-fn) 82 | 83 | (def seed 84 | "Function bind setting, a map of cache keys to values that will be preloaded when cache is bound." 85 | :memento.core/seed) 86 | 87 | (def ^:deprecated guava 88 | "DEPRECATED: Cache setting value, now points to caffeine implementation" 89 | :memento.core/caffeine) 90 | 91 | (def caffeine 92 | "Cache setting value, type name of Caffeine cache implementation" 93 | :memento.core/caffeine) 94 | 95 | (def none 96 | "Cache setting value, type name of noop cache implementation" 97 | :memento.core/none) 98 | 99 | (def ^:deprecated concurrency 100 | "DEPRECATED: it does nothing in Caffeine implementation" 101 | :memento.core/concurrency) 102 | 103 | (def initial-capacity 104 | "Cache setting, supported by: caffeine, an int. 105 | 106 | Sets the minimum total size for the internal hash tables. Providing a large enough estimate 107 | at construction time avoids the need for expensive resizing operations later, 108 | but setting this value unnecessarily high wastes memory." 109 | :memento.core/initial-capacity) 110 | 111 | (def size< 112 | "Cache setting, supported by: caffeine, a long. 113 | 114 | Specifies the maximum number of entries the cache may contain. Some implementations might evict entries 115 | even before the number of entries reaches the limit." 116 | :memento.core/size<) 117 | 118 | (def ttl 119 | "Cache and Function bind setting, a duration. 120 | 121 | Specifies that each entry should be automatically removed from the cache once a duration 122 | has elapsed after the entry's creation, or the most recent replacement of its value via a put. 123 | 124 | Duration is specified by a number (of seconds) or a vector of a number and unit keyword. 125 | So ttl of 10 or [10 :s] is the same. See 'timeunits' var." 126 | :memento.core/ttl) 127 | 128 | (def fade 129 | "Cache and Function bind setting, a duration. 130 | 131 | Specifies that each entry should be automatically removed from the cache once a fixed duration 132 | has elapsed after the entry's creation, the most recent replacement of its value, or its last access. 133 | Access time is reset by all cache read and write operations. 134 | 135 | Duration is specified by a number (of seconds) or a vector of a number and unit keyword. 136 | So fade of 10 or [10 :s] is the same. See 'timeunits' var." 137 | :memento.core/fade) 138 | 139 | (def tags 140 | "Function bind setting. 141 | 142 | List of tags for this memoized bind." 143 | :memento.core/tags) 144 | 145 | (def id 146 | "Function bind setting. 147 | 148 | Id of the function bind. If you're memoizing a Var, this defaults to stringified var name, 149 | otherwise the ID is the function itself. 150 | 151 | This is useful to specify when you're using Cache implementation that stores data outside JVM, 152 | as they often need a name for each function's cache." 153 | :memento.core/id) 154 | 155 | (def timeunits 156 | "Timeunits keywords, corresponds with Durations class." 157 | {:ns TimeUnit/NANOSECONDS 158 | :us TimeUnit/MICROSECONDS 159 | :ms TimeUnit/MILLISECONDS 160 | :s TimeUnit/SECONDS 161 | :m TimeUnit/MINUTES 162 | :h TimeUnit/HOURS 163 | :d TimeUnit/DAYS}) 164 | 165 | (def cache 166 | "The key extracted from object/var meta and used as cache configuration when 167 | 1-arg memo is called or ns-scan based mounting is performed." 168 | :memento.core/cache) 169 | 170 | (def mount 171 | "The key extracted from object/var meta and used as mount configuration when 172 | 1-arg memo is called or ns-scan based mounting is performed." 173 | :memento.core/mount) 174 | 175 | (def ret-ex-fn 176 | "Cache and function bind setting, a function that is ran to process the throwable thrown by the function, 177 | (fn [fn-args throwable] throwable)." 178 | :memento.core/ret-ex-fn) 179 | -------------------------------------------------------------------------------- /doc/performance.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | Performance is not a dedicated goal of this library, but here's some numbers: 4 | 5 |  6 | 7 |  8 | 9 | ```clojure 10 | ; memoize is not thread-safe and doesn't have any features 11 | (def f-memoize (memoize identity)) 12 | ; clojure.core.memoize 13 | (def f-core-memo (ccm/memo identity)) 14 | ; memento 15 | (def f-memento (m/memo identity {::m/type ::m/caffeine})) 16 | ; memento caffeine variable expiry 17 | (def f-memento-var (m/memo identity {::m/type ::m/caffeine ::m/expiry memento.caffeine.config/meta-expiry})) 18 | ; memento light caffeine 19 | (def f-light-memento (m/memo identity {::m/type ::m/light-caffeine})) 20 | ``` 21 | ## Memoize 22 | 23 | #### All hits 24 | ```text 25 | (cc/bench (f-memoize 1)) 26 | Evaluation count : 2911575540 in 60 samples of 48526259 calls. 27 | Execution time mean : 18,520670 ns 28 | Execution time std-deviation : 0,632964 ns 29 | Execution time lower quantile : 18,041806 ns ( 2,5%) 30 | Execution time upper quantile : 20,272312 ns (97,5%) 31 | Overhead used : 1,997090 ns 32 | 33 | Found 2 outliers in 60 samples (3,3333 %) 34 | low-severe 2 (3,3333 %) 35 | Variance from outliers : 20,6200 % Variance is moderately inflated by outliers 36 | 37 | ``` 38 | 39 | #### 1M misses (426ns per miss) 40 | ```text 41 | (cc/bench (let [f-memoize (memoize identity)] 42 | (reduce #(f-memoize %2) (range 1000000)))) 43 | Evaluation count : 180 in 60 samples of 3 calls. 44 | Execution time mean : 426,691729 ms 45 | Execution time std-deviation : 31,649211 ms 46 | Execution time lower quantile : 407,433346 ms ( 2,5%) 47 | Execution time upper quantile : 500,285216 ms (97,5%) 48 | Overhead used : 1,997090 ns 49 | 50 | Found 9 outliers in 60 samples (15,0000 %) 51 | low-severe 5 (8,3333 %) 52 | low-mild 4 (6,6667 %) 53 | Variance from outliers : 55,1467 % Variance is severely inflated by outliers 54 | ``` 55 | 56 | ## Clojure Core Memoize 57 | 58 | #### All hits 59 | 60 | ```text 61 | (cc/bench (f-core-memo 1)) 62 | Evaluation count : 329229720 in 60 samples of 5487162 calls. 63 | Execution time mean : 180,803852 ns 64 | Execution time std-deviation : 3,880666 ns 65 | Execution time lower quantile : 177,830691 ns ( 2,5%) 66 | Execution time upper quantile : 189,061520 ns (97,5%) 67 | Overhead used : 1,997090 ns 68 | 69 | Found 6 outliers in 60 samples (10,0000 %) 70 | low-severe 3 (5,0000 %) 71 | low-mild 3 (5,0000 %) 72 | Variance from outliers : 9,4347 % Variance is slightly inflated by outliers 73 | ``` 74 | 75 | #### 1M misses (778 ns per miss) 76 | 77 | ```text 78 | (cc/bench (let [f-core-memo (ccm/memo identity)] 79 | (reduce #(f-core-memo %2) (range 1000000)))) 80 | Evaluation count : 120 in 60 samples of 2 calls. 81 | Execution time mean : 778,758053 ms 82 | Execution time std-deviation : 58,068726 ms 83 | Execution time lower quantile : 717,950541 ms ( 2,5%) 84 | Execution time upper quantile : 947,641405 ms (97,5%) 85 | Overhead used : 1,997090 ns 86 | 87 | Found 6 outliers in 60 samples (10,0000 %) 88 | low-severe 4 (6,6667 %) 89 | low-mild 2 (3,3333 %) 90 | Variance from outliers : 55,1627 % Variance is severely inflated by outliers 91 | ``` 92 | 93 | #### 1M misses for size 100 LRU cache (1811 ns per miss) 94 | 95 | ```text 96 | (cc/bench (let [f-core-memo (ccm/lru identity :lru/threshold 100)] 97 | (reduce #(f-core-memo %2) (range 1000000)))) 98 | Evaluation count : 60 in 60 samples of 1 calls. 99 | Execution time mean : 1,811235 sec 100 | Execution time std-deviation : 23,960121 ms 101 | Execution time lower quantile : 1,773504 sec ( 2,5%) 102 | Execution time upper quantile : 1,866470 sec (97,5%) 103 | Overhead used : 1,997090 ns 104 | 105 | Found 2 outliers in 60 samples (3,3333 %) 106 | low-severe 2 (3,3333 %) 107 | Variance from outliers : 1,6389 % Variance is slightly inflated by outliers 108 | 109 | ``` 110 | 111 | ## Memento 112 | 113 | #### All hits 114 | 115 | ```text 116 | (cc/bench (f-memento 1)) 117 | 118 | Evaluation count : 854138220 in 60 samples of 14235637 calls. 119 | Execution time mean : 70,745055 ns 120 | Execution time std-deviation : 2,570125 ns 121 | Execution time lower quantile : 68,659819 ns ( 2,5%) 122 | Execution time upper quantile : 74,128774 ns (97,5%) 123 | Overhead used : 1,970580 ns 124 | 125 | Found 2 outliers in 60 samples (3,3333 %) 126 | low-severe 1 (1,6667 %) 127 | low-mild 1 (1,6667 %) 128 | Variance from outliers : 22,2591 % Variance is moderately inflated by outliers 129 | 130 | 131 | ``` 132 | 133 | #### 1M misses (474 ns per miss) 134 | 135 | ```text 136 | (cc/bench (let [f-memento (m/memo identity {::m/type ::m/caffeine})] 137 | (reduce #(f-memento %2) (range 1000000)))) 138 | Evaluation count : 120 in 60 samples of 2 calls. 139 | Execution time mean : 474,650866 ms 140 | Execution time std-deviation : 76,082064 ms 141 | Execution time lower quantile : 365,465019 ms ( 2,5%) 142 | Execution time upper quantile : 641,739223 ms (97,5%) 143 | Overhead used : 1,992837 ns 144 | 145 | ``` 146 | 147 | #### 1M misses for size 100 LRU cache (338 ns per miss) 148 | 149 | ```text 150 | (cc/bench (let [f-memento (m/memo identity {::m/size< 100 ::m/type ::m/caffeine})] 151 | (reduce #(f-memento %2) (range 1000000)))) 152 | Evaluation count : 180 in 60 samples of 3 calls. 153 | Execution time mean : 338,339882 ms 154 | Execution time std-deviation : 15,865012 ms 155 | Execution time lower quantile : 321,764748 ms ( 2,5%) 156 | Execution time upper quantile : 370,249429 ms (97,5%) 157 | Overhead used : 1,970580 ns 158 | 159 | Found 4 outliers in 60 samples (6,6667 %) 160 | low-severe 3 (5,0000 %) 161 | low-mild 1 (1,6667 %) 162 | Variance from outliers : 33,5491 % Variance is moderately inflated by outliers 163 | 164 | 165 | ``` 166 | 167 | ## Memento Variable Expiry 168 | 169 | #### All hits 170 | 171 | ```text 172 | (cc/bench (f-memento-var 1)) 173 | 174 | Evaluation count : 453412980 in 60 samples of 7556883 calls. 175 | Execution time mean : 132,501700 ns 176 | Execution time std-deviation : 2,015071 ns 177 | Execution time lower quantile : 130,326931 ns ( 2,5%) 178 | Execution time upper quantile : 134,890796 ns (97,5%) 179 | Overhead used : 1,978672 ns 180 | 181 | ``` 182 | 183 | #### 1M misses (526 ns per miss) 184 | 185 | ```text 186 | (cc/bench (let [f-memento-var (m/memo identity {::m/type ::m/caffeine ::m/expiry memento.caffeine.config/meta-expiry})] 187 | (reduce #(f-memento-var %2) (range 1000000)))) 188 | Evaluation count : 120 in 60 samples of 2 calls. 189 | Execution time mean : 526,197766 ms 190 | Execution time std-deviation : 59,110910 ms 191 | Execution time lower quantile : 426,811124 ms ( 2,5%) 192 | Execution time upper quantile : 644,451645 ms (97,5%) 193 | Overhead used : 1,978672 ns 194 | 195 | ``` 196 | 197 | #### 1M misses for size 100 LRU cache (387 ns per miss) 198 | 199 | ```text 200 | (cc/bench (let [f-memento-var (m/memo identity {::m/size< 100 ::m/type ::m/caffeine ::m/expiry memento.caffeine.config/meta-expiry})] 201 | (reduce #(f-memento-var %2) (range 1000000)))) 202 | Evaluation count : 180 in 60 samples of 3 calls. 203 | Execution time mean : 423,554590 ms 204 | Execution time std-deviation : 7,825220 ms 205 | Execution time lower quantile : 414,372683 ms ( 2,5%) 206 | Execution time upper quantile : 435,451863 ms (97,5%) 207 | Overhead used : 1,978672 ns 208 | 209 | ``` -------------------------------------------------------------------------------- /java/memento/caffeine/CaffeineCache_.java: -------------------------------------------------------------------------------- 1 | package memento.caffeine; 2 | 3 | import clojure.lang.*; 4 | import com.github.benmanes.caffeine.cache.Cache; 5 | import com.github.benmanes.caffeine.cache.Caffeine; 6 | import com.github.benmanes.caffeine.cache.RemovalListener; 7 | import com.github.benmanes.caffeine.cache.stats.CacheStats; 8 | import memento.base.CacheKey; 9 | import memento.base.EntryMeta; 10 | import memento.base.LockoutMap; 11 | import memento.base.Segment; 12 | 13 | import java.util.*; 14 | import java.util.concurrent.ConcurrentHashMap; 15 | import java.util.concurrent.ConcurrentMap; 16 | import java.util.function.BiFunction; 17 | 18 | public class CaffeineCache_ { 19 | 20 | private final BiFunction keyFn; 21 | 22 | private final SecondaryIndex secIndex; 23 | private final IFn retFn; 24 | 25 | private final IFn retExFn; 26 | 27 | private final Cache delegate; 28 | 29 | private final Set loads = ConcurrentHashMap.newKeySet(); 30 | 31 | public CaffeineCache_(Caffeine builder, final IFn keyFn, final IFn retFn, final IFn retExFn, SecondaryIndex secIndex) { 32 | this.keyFn = keyFn == null ? 33 | (segment, args) -> new CacheKey(segment.getId(), segment.getKeyFn().invoke(args)) : 34 | (segment, args) -> new CacheKey(segment.getId(), keyFn.invoke(segment.getKeyFn().invoke(args))); 35 | this.retFn = retFn; 36 | this.delegate = builder.build(); 37 | this.secIndex = secIndex; 38 | this.retExFn = retExFn; 39 | } 40 | 41 | private void initLoad(SpecialPromise promise) { 42 | promise.init(); 43 | loads.add(promise); 44 | } 45 | 46 | public Object cached(Segment segment, ISeq args) throws Throwable { 47 | CacheKey key = keyFn.apply(segment, args); 48 | do { 49 | SpecialPromise p = new SpecialPromise(); 50 | // check for ongoing load 51 | Object cached = delegate.asMap().putIfAbsent(key, p); 52 | if (cached == null) { 53 | try { 54 | initLoad(p); 55 | // calculate value 56 | Object result = AFn.applyToHelper(segment.getF(), args); 57 | if (retFn != null) { 58 | result = retFn.invoke(args, result); 59 | } 60 | if (!p.deliver(result)) { 61 | // The SpecialPromise was invalidated, restart the process 62 | delegate.asMap().remove(key, p); 63 | continue; 64 | } 65 | // if valid add to secondary index 66 | secIndex.add(key, result); 67 | if (result instanceof EntryMeta && ((EntryMeta) result).isNoCache()) { 68 | delegate.asMap().remove(key, p); 69 | } else { 70 | delegate.asMap().replace(key, p, result == null ? EntryMeta.NIL : result); 71 | } 72 | return EntryMeta.unwrap(result); 73 | } catch (Throwable t) { 74 | delegate.asMap().remove(key, p); 75 | if (!p.isInvalid()) { 76 | p.deliverException(retExFn == null ? t : (Throwable) retExFn.invoke(args, t)); 77 | throw t; 78 | } 79 | } finally { 80 | p.releaseResult(); 81 | loads.remove(p); 82 | } 83 | } else { 84 | // join into ongoing load 85 | if (cached instanceof SpecialPromise) { 86 | SpecialPromise sp = (SpecialPromise) cached; 87 | Object ret = sp.await(key); 88 | if (ret != EntryMeta.absent && !LockoutMap.awaitLockout(ret)) { 89 | // if not invalidated, return the value 90 | return EntryMeta.unwrap(ret); 91 | } 92 | } else { 93 | if (!LockoutMap.awaitLockout(cached)) { 94 | // if not invalidated, return the value 95 | return EntryMeta.unwrap(cached); 96 | } 97 | } 98 | // else try to initiate load again 99 | } 100 | } while (true); 101 | } 102 | 103 | public Object ifCached(Segment segment, ISeq args) throws Throwable { 104 | CacheKey key = keyFn.apply(segment, args); 105 | Object v = delegate.getIfPresent(key); 106 | Object absent = EntryMeta.absent; 107 | if (v == null) { 108 | return absent; 109 | } else if (v instanceof SpecialPromise) { 110 | SpecialPromise p = (SpecialPromise) v; 111 | Object ret = p.getNow(); 112 | if (ret == absent || LockoutMap.awaitLockout(ret)) { 113 | return absent; 114 | } else { 115 | return EntryMeta.unwrap(ret); 116 | } 117 | } else { 118 | return LockoutMap.awaitLockout(v) ? absent : EntryMeta.unwrap(v); 119 | } 120 | } 121 | 122 | public void invalidate(Segment segment) { 123 | final Iterator> iter = delegate.asMap().entrySet().iterator(); 124 | while (iter.hasNext()) { 125 | Map.Entry it = iter.next(); 126 | if (it.getKey().getId().equals(segment.getId())) { 127 | Object v = it.getValue(); 128 | if (v instanceof SpecialPromise) { 129 | ((SpecialPromise) v).invalidate(); 130 | } 131 | iter.remove(); 132 | } 133 | } 134 | } 135 | 136 | public void invalidate(Segment segment, ISeq args) { 137 | Object v = delegate.asMap().remove(keyFn.apply(segment, args)); 138 | if (v instanceof SpecialPromise) { 139 | ((SpecialPromise) v).invalidate(); 140 | } 141 | } 142 | 143 | public void invalidateAll() { 144 | delegate.invalidateAll(); 145 | } 146 | 147 | public void invalidateIds(Iterable ids) { 148 | HashSet keys = new HashSet<>(); 149 | for (Object id : ids) { 150 | secIndex.drainKeys(id, keys::add); 151 | } 152 | ConcurrentMap map = delegate.asMap(); 153 | for (CacheKey k : keys) { 154 | Object removed = map.remove(k); 155 | if (removed instanceof SpecialPromise) { 156 | ((SpecialPromise) removed).invalidate(); 157 | } 158 | } 159 | loads.forEach(row -> row.addInvalidIds(ids)); 160 | } 161 | 162 | public void addEntries(Segment segment, IPersistentMap argsToVals) { 163 | for (Object o : argsToVals) { 164 | MapEntry entry = (MapEntry) o; 165 | CacheKey key = keyFn.apply(segment, RT.seq(entry.getKey())); 166 | Object val = entry.getValue(); 167 | secIndex.add(key, val); 168 | delegate.put(key, val == null ? EntryMeta.NIL : val); 169 | } 170 | } 171 | 172 | public ConcurrentMap asMap() { 173 | return delegate.asMap(); 174 | } 175 | 176 | public CacheStats stats() { 177 | return delegate.stats(); 178 | } 179 | 180 | public void loadData(Map map) { 181 | map.forEach((Object k, Object v) -> { 182 | List list = (List) k; 183 | CacheKey key = new CacheKey(list.get(0), list.get(1)); 184 | secIndex.add(key, v); 185 | delegate.put(key, v == null ? EntryMeta.NIL : v); 186 | }); 187 | } 188 | 189 | public static RemovalListener listener(IFn removalListener) { 190 | return (k, v, removalCause) -> { 191 | if (!(v instanceof SpecialPromise)) { 192 | removalListener.invoke(k.getId(), k.getArgs(), v instanceof EntryMeta ? ((EntryMeta) v).getV() : v, removalCause); 193 | } 194 | }; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /java/memento/multi/TieredCache.java: -------------------------------------------------------------------------------- 1 | package memento.multi; 2 | 3 | import clojure.lang.ArraySeq; 4 | import clojure.lang.IFn; 5 | import clojure.lang.IPersistentMap; 6 | import clojure.lang.ISeq; 7 | import memento.base.ICache; 8 | import memento.base.Segment; 9 | 10 | public class TieredCache extends MultiCache { 11 | public TieredCache(ICache cache, ICache upstream, IPersistentMap conf, Object absent) { 12 | super(cache, upstream, conf, absent); 13 | } 14 | 15 | @Override 16 | public Object cached(Segment segment, ISeq args) { 17 | return cache.cached(segment.withFn(new AskUpstream(segment)), args); 18 | } 19 | 20 | private class AskUpstream implements IFn { 21 | 22 | private final Segment segment; 23 | 24 | public AskUpstream(Segment segment) { 25 | this.segment = segment; 26 | } 27 | 28 | @Override 29 | public Object call() { 30 | return upstream.cached(segment, ArraySeq.create()); 31 | } 32 | 33 | @Override 34 | public void run() { 35 | upstream.cached(segment, ArraySeq.create()); 36 | } 37 | 38 | @Override 39 | public Object invoke() { 40 | return upstream.cached(segment, ArraySeq.create()); 41 | } 42 | 43 | @Override 44 | public Object invoke(Object arg1) { 45 | return upstream.cached(segment, ArraySeq.create(arg1)); 46 | } 47 | 48 | @Override 49 | public Object invoke(Object arg1, Object arg2) { 50 | return upstream.cached(segment, ArraySeq.create(arg1, arg2)); 51 | } 52 | 53 | @Override 54 | public Object invoke(Object arg1, Object arg2, Object arg3) { 55 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3)); 56 | } 57 | 58 | @Override 59 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 60 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4)); 61 | } 62 | 63 | @Override 64 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 65 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5)); 66 | } 67 | 68 | @Override 69 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 70 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6)); 71 | } 72 | 73 | @Override 74 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 75 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7)); 76 | } 77 | 78 | @Override 79 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 80 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)); 81 | } 82 | 83 | @Override 84 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 85 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)); 86 | } 87 | 88 | @Override 89 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 90 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); 91 | } 92 | 93 | @Override 94 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 95 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11)); 96 | } 97 | 98 | @Override 99 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 100 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12)); 101 | } 102 | 103 | @Override 104 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 105 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13)); 106 | } 107 | 108 | @Override 109 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 110 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14)); 111 | } 112 | 113 | @Override 114 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 115 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15)); 116 | } 117 | 118 | @Override 119 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 120 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16)); 121 | } 122 | 123 | @Override 124 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 125 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17)); 126 | } 127 | 128 | @Override 129 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 130 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18)); 131 | } 132 | 133 | @Override 134 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 135 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19)); 136 | } 137 | 138 | @Override 139 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 140 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20)); 141 | } 142 | 143 | @Override 144 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 145 | Object[] allArgs = new Object[20 + args.length]; 146 | System.arraycopy(args, 0, allArgs, 20, args.length); 147 | allArgs[0] = arg1; 148 | allArgs[1] = arg2; 149 | allArgs[2] = arg3; 150 | allArgs[3] = arg4; 151 | allArgs[4] = arg5; 152 | allArgs[5] = arg6; 153 | allArgs[6] = arg7; 154 | allArgs[7] = arg8; 155 | allArgs[8] = arg9; 156 | allArgs[9] = arg10; 157 | allArgs[10] = arg11; 158 | allArgs[11] = arg12; 159 | allArgs[12] = arg13; 160 | allArgs[13] = arg14; 161 | allArgs[14] = arg15; 162 | allArgs[15] = arg16; 163 | allArgs[16] = arg17; 164 | allArgs[17] = arg18; 165 | allArgs[18] = arg19; 166 | allArgs[19] = arg20; 167 | return upstream.cached(segment, ArraySeq.create(allArgs)); 168 | } 169 | 170 | @Override 171 | public Object applyTo(ISeq arglist) { 172 | return upstream.cached(segment, arglist); 173 | } 174 | 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /java/memento/mount/CachedFn.java: -------------------------------------------------------------------------------- 1 | package memento.mount; 2 | 3 | import clojure.lang.*; 4 | import memento.base.ICache; 5 | import memento.base.Segment; 6 | 7 | public class CachedFn extends AFunction implements IMountPoint, Cached { 8 | private final Object reloadGuard; 9 | private final IFn originalFn; 10 | private final Segment segment; 11 | 12 | @Override 13 | public IPersistentMap asMap() { 14 | return mp.asMap(); 15 | } 16 | 17 | @Override 18 | public Object cached(ISeq args) { 19 | return mp.cached(args); 20 | } 21 | 22 | @Override 23 | public Object ifCached(ISeq args) { 24 | return mp.ifCached(args); 25 | } 26 | 27 | @Override 28 | public Object getTags() { 29 | return mp.getTags(); 30 | } 31 | 32 | @Override 33 | public Object handleEvent(Object event) { 34 | return mp.handleEvent(event); 35 | } 36 | 37 | @Override 38 | public ICache invalidate(ISeq args) { 39 | return mp.invalidate(args); 40 | } 41 | 42 | @Override 43 | public ICache invalidateAll() { 44 | return mp.invalidateAll(); 45 | } 46 | 47 | @Override 48 | public ICache mountedCache() { 49 | return mp.mountedCache(); 50 | } 51 | 52 | @Override 53 | public Segment segment() { 54 | return mp.segment(); 55 | } 56 | 57 | @Override 58 | public ICache addEntries(IPersistentMap argsToVals) { 59 | return mp.addEntries(argsToVals); 60 | } 61 | 62 | private final IMountPoint mp; 63 | private final IPersistentMap meta; 64 | 65 | public CachedFn(Object reloadGuard, IMountPoint mp, IPersistentMap meta, IFn originalFn) { 66 | this.reloadGuard = reloadGuard; 67 | this.mp = mp; 68 | this.meta = meta; 69 | this.originalFn = originalFn; 70 | this.segment = mp.segment(); 71 | } 72 | 73 | @Override 74 | public IPersistentMap meta() { 75 | return meta; 76 | } 77 | 78 | @Override 79 | public IObj withMeta(IPersistentMap meta) { 80 | return new CachedFn(reloadGuard, mp, meta, originalFn); 81 | } 82 | 83 | @Override 84 | public Object call() { 85 | return mp.mountedCache().cached(segment, ArraySeq.create()); 86 | } 87 | 88 | @Override 89 | public void run() { 90 | mp.mountedCache().cached(segment, ArraySeq.create()); 91 | } 92 | 93 | @Override 94 | public Object invoke() { 95 | return mp.mountedCache().cached(segment, ArraySeq.create()); 96 | } 97 | 98 | @Override 99 | public Object invoke(Object arg1) { 100 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1)); 101 | } 102 | 103 | @Override 104 | public Object invoke(Object arg1, Object arg2) { 105 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2)); 106 | } 107 | 108 | @Override 109 | public Object invoke(Object arg1, Object arg2, Object arg3) { 110 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3)); 111 | } 112 | 113 | @Override 114 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 115 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4)); 116 | } 117 | 118 | @Override 119 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 120 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5)); 121 | } 122 | 123 | @Override 124 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 125 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6)); 126 | } 127 | 128 | @Override 129 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 130 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7)); 131 | } 132 | 133 | @Override 134 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 135 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)); 136 | } 137 | 138 | @Override 139 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 140 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)); 141 | } 142 | 143 | @Override 144 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 145 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); 146 | } 147 | 148 | @Override 149 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 150 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11)); 151 | } 152 | 153 | @Override 154 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 155 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12)); 156 | } 157 | 158 | @Override 159 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 160 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13)); 161 | } 162 | 163 | @Override 164 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 165 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14)); 166 | } 167 | 168 | @Override 169 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 170 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15)); 171 | } 172 | 173 | @Override 174 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 175 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16)); 176 | } 177 | 178 | @Override 179 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 180 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17)); 181 | } 182 | 183 | @Override 184 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 185 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18)); 186 | } 187 | 188 | @Override 189 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 190 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19)); 191 | } 192 | 193 | @Override 194 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 195 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20)); 196 | } 197 | 198 | @Override 199 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 200 | Object[] allArgs = new Object[20 + args.length]; 201 | System.arraycopy(args, 0, allArgs, 20, args.length); 202 | allArgs[0] = arg1; 203 | allArgs[1] = arg2; 204 | allArgs[2] = arg3; 205 | allArgs[3] = arg4; 206 | allArgs[4] = arg5; 207 | allArgs[5] = arg6; 208 | allArgs[6] = arg7; 209 | allArgs[7] = arg8; 210 | allArgs[8] = arg9; 211 | allArgs[9] = arg10; 212 | allArgs[10] = arg11; 213 | allArgs[11] = arg12; 214 | allArgs[12] = arg13; 215 | allArgs[13] = arg14; 216 | allArgs[14] = arg15; 217 | allArgs[15] = arg16; 218 | allArgs[16] = arg17; 219 | allArgs[17] = arg18; 220 | allArgs[18] = arg19; 221 | allArgs[19] = arg20; 222 | return mp.mountedCache().cached(segment, ArraySeq.create(allArgs)); 223 | } 224 | 225 | @Override 226 | public Object applyTo(ISeq arglist) { 227 | return mp.mountedCache().cached(segment, arglist); 228 | } 229 | 230 | public IMountPoint getMp() { 231 | return mp; 232 | } 233 | 234 | public IFn getOriginalFn() { 235 | return originalFn; 236 | } 237 | 238 | @Override 239 | public String toString() { 240 | return "CachedFn{" + 241 | "originalFn=" + originalFn + 242 | ", segment=" + segment + 243 | ", mp=" + mp + 244 | ", meta=" + meta + 245 | '}'; 246 | } 247 | 248 | public Segment getSegment() { 249 | return segment; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/memento/core.clj: -------------------------------------------------------------------------------- 1 | (ns memento.core 2 | "Memoization library." 3 | {:author "Rok Lenarčič"} 4 | (:require [memento.base :as base] 5 | [memento.caffeine] 6 | [memento.multi :as multi] 7 | [memento.mount :as mount]) 8 | (:import (java.util IdentityHashMap) 9 | (java.util.function BiFunction) 10 | (memento.base EntryMeta ICache LockoutTag) 11 | (memento.mount Cached IMountPoint))) 12 | 13 | (defn do-not-cache 14 | "Wrap a function result value in a wrapper that tells the Cache not to 15 | cache this particular value." 16 | [v] 17 | (if (instance? EntryMeta v) 18 | (do (.setNoCache ^EntryMeta v true) v) 19 | (EntryMeta. v true #{}))) 20 | 21 | (defn with-tag-id 22 | "Wrap a function result value in a wrapper that has the given additional 23 | tag + ID information. You can add multiple IDs for same tag. 24 | 25 | This information is later used by memo-clear-tag!." 26 | [v tag id] 27 | (if (instance? EntryMeta v) 28 | (do (.setTagIdents ^EntryMeta v (conj (.getTagIdents ^EntryMeta v) [tag id])) v) 29 | (EntryMeta. v false #{[tag id]}))) 30 | 31 | (defn create 32 | "Create a cache. 33 | 34 | A conf is a map of cache settings, see memento.config namespace for names of settings." 35 | [conf] 36 | (base/base-create-cache conf)) 37 | 38 | (defn bind 39 | "Bind the cache to a function or a var. If a var is specified, then var root 40 | binding is modified. 41 | 42 | The mount-conf is a configuration options for mount point. 43 | 44 | It can be a map with options, a vector of tags, or one tag. 45 | 46 | Supported options are: 47 | - memento.core/key-fn 48 | - memento.core/key-fn* 49 | - memento.core/ret-fn 50 | - memento.core/tags 51 | - memento.core/seed" 52 | [fn-or-var mount-conf cache] 53 | (when-not (instance? ICache cache) 54 | (throw (IllegalArgumentException. "Argument should satisfy memento.base/Cache"))) 55 | (mount/bind fn-or-var mount-conf cache)) 56 | 57 | (defn memo 58 | "Combines cache create and bind operations from this namespace. 59 | 60 | If conf is provided, it is used as mount-conf in bind operation, but with any extra map keys 61 | going into cache create configuration. 62 | 63 | If no configuration is provided, meta of the fn or var is examined. 64 | 65 | The value of :memento.core/cache meta key is used as conf parameter 66 | in memento.core/memo. If :memento.core/mount key is also present, then 67 | they are used as cache and conf parameters respectively." 68 | ([fn-or-var] 69 | (let [{::keys [mount cache]} (meta fn-or-var)] 70 | (if mount (memo fn-or-var mount cache) 71 | (memo fn-or-var cache)))) 72 | ([fn-or-var conf] 73 | (if (map? conf) 74 | (memo fn-or-var 75 | (select-keys conf mount/configuration-props) 76 | (apply dissoc conf mount/configuration-props)) 77 | (memo fn-or-var conf {}))) 78 | ([fn-or-var mount-conf cache-conf] 79 | (->> cache-conf 80 | create 81 | (bind fn-or-var mount-conf)))) 82 | 83 | (defmacro defmemo 84 | "Like defn, but immediately wraps var in a memo call. It expects caching configuration 85 | to be in meta under memento.core/cache key, as expected by memo." 86 | {:arglists '([name doc-string? attr-map? [params*] prepost-map? body] 87 | [name doc-string? attr-map? ([params*] prepost-map? body)+ attr-map?])} 88 | [& body] 89 | `(memo (defn ~@body))) 90 | 91 | (defn active-cache 92 | "Return Cache instance from the function, if present." 93 | [f] (some-> (mount/mount-point f) mount/mounted-cache)) 94 | 95 | (defn memoized? 96 | "Returns true if function is memoized." 97 | [f] (instance? Cached f)) 98 | 99 | (defn memo-unwrap 100 | "Takes a function and returns an uncached function." 101 | [f] (if (instance? Cached f) (.getOriginalFn ^Cached f) f)) 102 | 103 | (defn memo-clear-cache! 104 | "Invalidate all entries in Cache. Returns cache." 105 | [cache] 106 | (when-not (instance? ICache cache) 107 | (throw (IllegalArgumentException. "Argument should satisfy memento.base/Cache"))) 108 | (base/invalidate-all cache)) 109 | 110 | (defn none-cache? 111 | "Returns true if this cache is one that does no caching." 112 | [cache] 113 | (= cache base/no-cache)) 114 | 115 | (defn memo-clear! 116 | "Invalidate one entry (f with arglist) on memoized function f, 117 | or invalidate all entries for memoized function. Returns f." 118 | ([f] 119 | (when-let [^IMountPoint mp (mount/mount-point f)] (.invalidateAll mp)) 120 | f) 121 | ([f & fargs] 122 | (when-let [^IMountPoint mp (mount/mount-point f)] (.invalidate mp fargs)) 123 | f)) 124 | 125 | (defn memo-add! 126 | "Add map's entries to the cache. The keys are argument-lists. 127 | 128 | Returns f." 129 | [f m] 130 | (when-let [^IMountPoint mp (mount/mount-point f)] (.addEntries mp m)) 131 | f) 132 | 133 | (defn as-map 134 | "Return a map representation of the memoized entries on this function." 135 | [f] 136 | (when-let [^IMountPoint mp (mount/mount-point f)] (.asMap mp))) 137 | 138 | (defn tags 139 | "Return tags of the memoized function." 140 | [f] 141 | (when-let [^IMountPoint mp (mount/mount-point f)] (.getTags mp))) 142 | 143 | (defn mounts-by-tag 144 | "Returns a sequence of MountPoint instances used by memoized functions which are tagged by this tag." 145 | [tag] 146 | (get @mount/tags tag [])) 147 | 148 | (defn caches-by-tag 149 | "Returns a collection of distinct caches that are mounted with a tag" 150 | [tag] 151 | (let [m (IdentityHashMap.)] 152 | (run! #(.put m (mount/mounted-cache %) nil) (mounts-by-tag tag)) 153 | (.keySet m))) 154 | 155 | (defn fire-event! 156 | "Fire an event payload to the single cached function or all tagged functions, if tag 157 | is provided." 158 | [f-or-tag evt] 159 | (if (instance? IMountPoint f-or-tag) 160 | (.handleEvent ^IMountPoint f-or-tag evt) 161 | (->> (mounts-by-tag f-or-tag) 162 | (eduction (map #(.handleEvent ^IMountPoint % evt))) 163 | dorun))) 164 | 165 | (defn memo-clear-tags! 166 | "Invalidate all entries that have the specified tag + id metadata. ID can be anything. 167 | 168 | Expects a collection of [tag id] pairs." 169 | [& tag+ids] 170 | (let [cache->ids (IdentityHashMap.) 171 | _ (doseq [[tag tag+ids] (group-by first tag+ids) 172 | cache (caches-by-tag tag)] 173 | (.compute 174 | cache->ids 175 | cache 176 | (reify BiFunction 177 | (apply [this k v] (into (or v []) tag+ids))))) 178 | tag (LockoutTag.)] 179 | (try 180 | (.startLockout base/lockout-map tag+ids tag) 181 | (run! (fn [e] (base/invalidate-ids (key e) (val e))) cache->ids) 182 | (finally 183 | (.endLockout base/lockout-map tag+ids tag))))) 184 | 185 | (defn memo-clear-tag! 186 | "Invalidate all entries that have the specified tag + id metadata. ID can be anything." 187 | [tag id] 188 | (memo-clear-tags! [tag id])) 189 | 190 | (defn update-tag-caches! 191 | "For each memoized function with the specified tag, set the Cache used by the fn to (cache-fn current-cache). 192 | 193 | Cache update function is ran on each 194 | memoized function (mount point), so if one cache is backing multiple functions, the cache update function is called 195 | multiple times on it. If you want to run cache-fn one each Cache instance only once, I recommend wrapping it 196 | in clojure.core/memoize. 197 | 198 | If caches are thread-bound to a different value with with-caches, then those 199 | bindings are modified instead of root bindings." 200 | [tag cache-fn] 201 | (mount/alter-caches-mapping tag mount/update-existing cache-fn)) 202 | 203 | (defmacro with-caches 204 | "Within the block, each memoized function with the specified tag has its cache update by cache-fn. 205 | 206 | The values are bound within the block as a thread local binding. Cache update function is ran on each 207 | memoized function (mount point), so if one cache is backing multiple functions, the cache update function is called 208 | multiple timed on it. If you want to run cache-fn one each Cache instance only once, I recommend wrapping it 209 | in clojure.core/memoize." 210 | [tag cache-fn & body] 211 | `(binding [mount/*caches* (mount/update-existing mount/*caches* (get @mount/tags ~tag []) ~cache-fn)] 212 | ~@body)) 213 | 214 | (defn evt-cache-add 215 | "Convenience function. It creates or wraps event handler fn, 216 | with an implementation which expects an event to be a vector of 217 | [event-type payload], it checks for matching event type and inserts 218 | the result of (->entries payload) into the cache." 219 | ([evt-type ->entries] (evt-cache-add (constantly nil) evt-type ->entries)) 220 | ([evt-fn evt-type ->entries] 221 | (fn [mountp evt] 222 | (when (and (vector? evt) 223 | (= (count evt) 2) 224 | (= evt-type (first evt))) 225 | (memo-add! mountp (->entries (second evt)))) 226 | (evt-fn mountp evt)))) 227 | 228 | (defn tiered 229 | "Creates a configuration for a tiered cache. Both parameters are either a conf map or a cache. 230 | 231 | Entry is fetched from cache, delegating to upstream is not found. After the operation 232 | the entry is in both caches. 233 | 234 | Useful when upstream is a big cache that outside the JVM, but it's not that inexpensive, so you 235 | want a local smaller cache in front of it. 236 | 237 | Invalidation operations also affect upstream. Other operations only affect local cache." 238 | [cache upstream] 239 | {::type ::tiered 240 | ::multi/cache cache 241 | ::multi/upstream upstream}) 242 | 243 | (defn consulting 244 | "Creates a configuration for a consulting tiered cache. Both parameters are either a conf map or a cache. 245 | 246 | Entry is fetched from cache, if not found, the upstream is asked for entry if present (but not to make one 247 | in the upstream). 248 | 249 | After the operation, the entry is in local cache, upstream is unchanged. 250 | 251 | Useful when you want to consult a long term upstream cache for existing entries, but you don't want any 252 | entries being created for the short term cache to be pushed upstream. 253 | 254 | Invalidation operations also affect upstream. Other operations only affect local cache." 255 | [cache upstream] 256 | {::type ::consulting 257 | ::multi/cache cache 258 | ::multi/upstream upstream}) 259 | 260 | (defn daisy 261 | "Creates a configuration for a daisy chained cache. Cache parameter is a conf map or a cache. 262 | 263 | Entry is returned from cache IF PRESENT, otherwise upstream is hit. The returned value 264 | is NOT added to cache. 265 | 266 | After the operation the entry is either in local or upstream cache. 267 | 268 | Useful when you don't want entries from upstream accumulating in local 269 | cache, and you're feeding the local cache via some other means: 270 | - a preloaded fixed cache 271 | - manually adding entries 272 | 273 | Invalidation operations also affect upstream. Other operations only affect local cache." 274 | [cache upstream] 275 | {::type ::daisy 276 | ::multi/cache cache 277 | ::multi/upstream upstream}) 278 | 279 | (defmacro if-cached 280 | "Like if-let, but then clause is executed if the call in the binding is cached, with the binding symbol 281 | being bound to the cached value. 282 | 283 | This assumes that the top form in bindings is a call of cached function, generating an error otherwise. 284 | 285 | e.g. (if-cached [my-val (my-cached-fn arg1)] ...)" 286 | ([bindings then] 287 | `(if-cached ~bindings ~then nil)) 288 | ([bindings then else] 289 | (assert (vector? bindings)) 290 | (assert (= 2 (count bindings))) 291 | (let [form (bindings 0) 292 | cache-call (bindings 1) 293 | _ (assert (list? cache-call)) 294 | f (first cache-call) 295 | _ (assert (symbol? f))] 296 | `(if-let [mnt# (mount/mount-point ~(first cache-call))] 297 | (let [mnt# (if (instance? Cached mnt#) (.getMp mnt#) mnt#) 298 | cached# (.ifCached mnt# '~(next cache-call))] 299 | (if (= cached# base/absent) 300 | ~else 301 | (let [~form cached#] ~then))) 302 | (throw (ex-info (str "Function " ~(str f) " is not a cached function") 303 | {:form '~cache-call})))))) 304 | -------------------------------------------------------------------------------- /java/memento/multi/ConsultingCache.java: -------------------------------------------------------------------------------- 1 | package memento.multi; 2 | 3 | import clojure.lang.*; 4 | import memento.base.ICache; 5 | import memento.base.Segment; 6 | 7 | public class ConsultingCache extends MultiCache { 8 | public ConsultingCache(ICache cache, ICache upstream, IPersistentMap conf, Object absent) { 9 | super(cache, upstream, conf, absent); 10 | } 11 | 12 | @Override 13 | public Object cached(Segment segment, ISeq args) { 14 | return cache.cached(segment.withFn(new UpstreamOrCalc(segment)), args); 15 | } 16 | 17 | private class UpstreamOrCalc implements IFn { 18 | 19 | private Segment segment; 20 | 21 | public UpstreamOrCalc(Segment segment) { 22 | this.segment = segment; 23 | } 24 | 25 | @Override 26 | public Object call() { 27 | ISeq s = ArraySeq.create(); 28 | Object up = upstream.ifCached(segment, s); 29 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 30 | } 31 | 32 | @Override 33 | public void run() { 34 | ISeq s = ArraySeq.create(); 35 | Object up = upstream.ifCached(segment, s); 36 | if (up == absent) { 37 | AFn.applyToHelper(segment.getF(), s); 38 | } 39 | } 40 | 41 | @Override 42 | public Object invoke() { 43 | ISeq s = ArraySeq.create(); 44 | Object up = upstream.ifCached(segment, s); 45 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 46 | } 47 | 48 | @Override 49 | public Object invoke(Object arg1) { 50 | ISeq s = ArraySeq.create(arg1); 51 | Object up = upstream.ifCached(segment, s); 52 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 53 | } 54 | 55 | @Override 56 | public Object invoke(Object arg1, Object arg2) { 57 | ISeq s = ArraySeq.create(arg1, arg2); 58 | Object up = upstream.ifCached(segment, s); 59 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 60 | } 61 | 62 | @Override 63 | public Object invoke(Object arg1, Object arg2, Object arg3) { 64 | ISeq s = ArraySeq.create(arg1, arg2, arg3); 65 | Object up = upstream.ifCached(segment, s); 66 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 67 | } 68 | 69 | @Override 70 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 71 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4); 72 | Object up = upstream.ifCached(segment, s); 73 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 74 | } 75 | 76 | @Override 77 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 78 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5); 79 | Object up = upstream.ifCached(segment, s); 80 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 81 | } 82 | 83 | @Override 84 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 85 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6); 86 | Object up = upstream.ifCached(segment, s); 87 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 88 | } 89 | 90 | @Override 91 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 92 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7); 93 | Object up = upstream.ifCached(segment, s); 94 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 95 | } 96 | 97 | @Override 98 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 99 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); 100 | Object up = upstream.ifCached(segment, s); 101 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 102 | } 103 | 104 | @Override 105 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 106 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); 107 | Object up = upstream.ifCached(segment, s); 108 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 109 | } 110 | 111 | @Override 112 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 113 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10); 114 | Object up = upstream.ifCached(segment, s); 115 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 116 | } 117 | 118 | @Override 119 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 120 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11); 121 | Object up = upstream.ifCached(segment, s); 122 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 123 | } 124 | 125 | @Override 126 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 127 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12); 128 | Object up = upstream.ifCached(segment, s); 129 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 130 | } 131 | 132 | @Override 133 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 134 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13); 135 | Object up = upstream.ifCached(segment, s); 136 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 137 | } 138 | 139 | @Override 140 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 141 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14); 142 | Object up = upstream.ifCached(segment, s); 143 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 144 | } 145 | 146 | @Override 147 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 148 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15); 149 | Object up = upstream.ifCached(segment, s); 150 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 151 | } 152 | 153 | @Override 154 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 155 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16); 156 | Object up = upstream.ifCached(segment, s); 157 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 158 | } 159 | 160 | @Override 161 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 162 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17); 163 | Object up = upstream.ifCached(segment, s); 164 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 165 | } 166 | 167 | @Override 168 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 169 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18); 170 | Object up = upstream.ifCached(segment, s); 171 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 172 | } 173 | 174 | @Override 175 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 176 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19); 177 | Object up = upstream.ifCached(segment, s); 178 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 179 | } 180 | 181 | @Override 182 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 183 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20); 184 | Object up = upstream.ifCached(segment, s); 185 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 186 | } 187 | 188 | @Override 189 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 190 | Object[] allArgs = new Object[20 + args.length]; 191 | System.arraycopy(args, 0, allArgs, 20, args.length); 192 | allArgs[0] = arg1; 193 | allArgs[1] = arg2; 194 | allArgs[2] = arg3; 195 | allArgs[3] = arg4; 196 | allArgs[4] = arg5; 197 | allArgs[5] = arg6; 198 | allArgs[6] = arg7; 199 | allArgs[7] = arg8; 200 | allArgs[8] = arg9; 201 | allArgs[9] = arg10; 202 | allArgs[10] = arg11; 203 | allArgs[11] = arg12; 204 | allArgs[12] = arg13; 205 | allArgs[13] = arg14; 206 | allArgs[14] = arg15; 207 | allArgs[15] = arg16; 208 | allArgs[16] = arg17; 209 | allArgs[17] = arg18; 210 | allArgs[18] = arg19; 211 | allArgs[19] = arg20; 212 | ISeq s = ArraySeq.create(allArgs); 213 | Object up = upstream.ifCached(segment, s); 214 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 215 | } 216 | 217 | @Override 218 | public Object applyTo(ISeq arglist) { 219 | Object up = upstream.ifCached(segment, arglist); 220 | return up == absent ? AFn.applyToHelper(segment.getF(), arglist) : up; 221 | } 222 | 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /java/memento/mount/CachedMultiFn.java: -------------------------------------------------------------------------------- 1 | package memento.mount; 2 | 3 | import clojure.lang.*; 4 | import memento.base.ICache; 5 | import memento.base.Segment; 6 | 7 | public class CachedMultiFn extends MultiFn implements IMountPoint, Cached, IObj { 8 | private final Object reloadGuard; 9 | private final MultiFn originalFn; 10 | private final Segment segment; 11 | 12 | @Override 13 | public IPersistentMap asMap() { 14 | return mp.asMap(); 15 | } 16 | 17 | @Override 18 | public Object cached(ISeq args) { 19 | return mp.cached(args); 20 | } 21 | 22 | @Override 23 | public Object ifCached(ISeq args) { 24 | return mp.ifCached(args); 25 | } 26 | 27 | @Override 28 | public Object getTags() { 29 | return mp.getTags(); 30 | } 31 | 32 | @Override 33 | public Object handleEvent(Object event) { 34 | return mp.handleEvent(event); 35 | } 36 | 37 | @Override 38 | public ICache invalidate(ISeq args) { 39 | return mp.invalidate(args); 40 | } 41 | 42 | @Override 43 | public ICache invalidateAll() { 44 | return mp.invalidateAll(); 45 | } 46 | 47 | @Override 48 | public ICache mountedCache() { 49 | return mp.mountedCache(); 50 | } 51 | 52 | @Override 53 | public Segment segment() { 54 | return mp.segment(); 55 | } 56 | 57 | @Override 58 | public ICache addEntries(IPersistentMap argsToVals) { 59 | return mp.addEntries(argsToVals); 60 | } 61 | 62 | private final String name; 63 | private final IMountPoint mp; 64 | private final IPersistentMap meta; 65 | 66 | public CachedMultiFn(String name, Object reloadGuard, IMountPoint mp, IPersistentMap meta, MultiFn originalFn) { 67 | super(name, originalFn.dispatchFn, originalFn.defaultDispatchVal, originalFn.hierarchy); 68 | this.reloadGuard = reloadGuard; 69 | this.mp = mp; 70 | this.name = name; 71 | this.meta = meta; 72 | this.originalFn = originalFn; 73 | this.segment = mp.segment(); 74 | } 75 | 76 | @Override 77 | public IPersistentMap meta() { 78 | return meta; 79 | } 80 | 81 | @Override 82 | public IObj withMeta(IPersistentMap meta) { 83 | return new CachedMultiFn(name, reloadGuard, mp, meta, originalFn); 84 | } 85 | 86 | @Override 87 | public Object call() { 88 | return mp.mountedCache().cached(segment, ArraySeq.create()); 89 | } 90 | 91 | @Override 92 | public void run() { 93 | mp.mountedCache().cached(segment, ArraySeq.create()); 94 | } 95 | 96 | @Override 97 | public Object invoke() { 98 | return mp.mountedCache().cached(segment, ArraySeq.create()); 99 | } 100 | 101 | @Override 102 | public Object invoke(Object arg1) { 103 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1)); 104 | } 105 | 106 | @Override 107 | public Object invoke(Object arg1, Object arg2) { 108 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2)); 109 | } 110 | 111 | @Override 112 | public Object invoke(Object arg1, Object arg2, Object arg3) { 113 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3)); 114 | } 115 | 116 | @Override 117 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 118 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4)); 119 | } 120 | 121 | @Override 122 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 123 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5)); 124 | } 125 | 126 | @Override 127 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 128 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6)); 129 | } 130 | 131 | @Override 132 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 133 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7)); 134 | } 135 | 136 | @Override 137 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 138 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)); 139 | } 140 | 141 | @Override 142 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 143 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)); 144 | } 145 | 146 | @Override 147 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 148 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); 149 | } 150 | 151 | @Override 152 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 153 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11)); 154 | } 155 | 156 | @Override 157 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 158 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12)); 159 | } 160 | 161 | @Override 162 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 163 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13)); 164 | } 165 | 166 | @Override 167 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 168 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14)); 169 | } 170 | 171 | @Override 172 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 173 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15)); 174 | } 175 | 176 | @Override 177 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 178 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16)); 179 | } 180 | 181 | @Override 182 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 183 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17)); 184 | } 185 | 186 | @Override 187 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 188 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18)); 189 | } 190 | 191 | @Override 192 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 193 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19)); 194 | } 195 | 196 | @Override 197 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 198 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20)); 199 | } 200 | 201 | @Override 202 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 203 | Object[] allArgs = new Object[20 + args.length]; 204 | System.arraycopy(args, 0, allArgs, 20, args.length); 205 | allArgs[0] = arg1; 206 | allArgs[1] = arg2; 207 | allArgs[2] = arg3; 208 | allArgs[3] = arg4; 209 | allArgs[4] = arg5; 210 | allArgs[5] = arg6; 211 | allArgs[6] = arg7; 212 | allArgs[7] = arg8; 213 | allArgs[8] = arg9; 214 | allArgs[9] = arg10; 215 | allArgs[10] = arg11; 216 | allArgs[11] = arg12; 217 | allArgs[12] = arg13; 218 | allArgs[13] = arg14; 219 | allArgs[14] = arg15; 220 | allArgs[15] = arg16; 221 | allArgs[16] = arg17; 222 | allArgs[17] = arg18; 223 | allArgs[18] = arg19; 224 | allArgs[19] = arg20; 225 | return mp.mountedCache().cached(segment, ArraySeq.create(allArgs)); 226 | } 227 | 228 | @Override 229 | public Object applyTo(ISeq arglist) { 230 | return mp.mountedCache().cached(segment, arglist); 231 | } 232 | 233 | public IMountPoint getMp() { 234 | return mp; 235 | } 236 | 237 | public IFn getOriginalFn() { 238 | return originalFn; 239 | } 240 | 241 | @Override 242 | public String toString() { 243 | return "CachedMultiFn{" + 244 | "originalFn=" + originalFn + 245 | ", segment=" + segment + 246 | ", mp=" + mp + 247 | ", meta=" + meta + 248 | '}'; 249 | } 250 | 251 | public Segment getSegment() { 252 | return segment; 253 | } 254 | 255 | @Override 256 | public MultiFn addMethod(Object dispatchVal, IFn method) { 257 | originalFn.addMethod(dispatchVal, method); 258 | return this; 259 | } 260 | 261 | @Override 262 | public IFn getMethod(Object dispatchVal) { 263 | return originalFn.getMethod(dispatchVal); 264 | } 265 | 266 | @Override 267 | public IPersistentMap getMethodTable() { 268 | return originalFn == null ? PersistentHashMap.EMPTY : originalFn.getMethodTable(); 269 | } 270 | 271 | @Override 272 | public IPersistentMap getPreferTable() { 273 | return originalFn.getPreferTable(); 274 | } 275 | 276 | @Override 277 | public MultiFn preferMethod(Object dispatchValX, Object dispatchValY) { 278 | originalFn.preferMethod(dispatchValX, dispatchValY); 279 | return this; 280 | } 281 | 282 | @Override 283 | public MultiFn removeMethod(Object dispatchVal) { 284 | originalFn.removeMethod(dispatchVal); 285 | return this; 286 | } 287 | 288 | @Override 289 | public MultiFn reset() { 290 | originalFn.reset(); 291 | return this; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Memento 2 | 3 | A library for function memoization with scoped caches and tagged eviction capabilities. 4 | 5 | ## Dependency 6 | 7 | [](https://clojars.org/org.clojars.roklenarcic/memento) 8 | 9 | ## Version 2.0 breaking changes 10 | 11 | Version 2 moves from Java 8 to Java 11 as minimum JVM version. Caffeine version is 3 instead of 2. 12 | 13 | ## Version 1.0 breaking changes 14 | 15 | Version 1.0 represents a switch from Guava to Caffeine, which is a faster caching library, with added 16 | benefit of not pulling in the whole Guava artefact which is more that just that Cache. The Guava Cache type 17 | key and the config namespace are deprecated and will be removed in the future. 18 | 19 | ## Motivation 20 | 21 | Why is there a need for another caching library? 22 | 23 | - request scoped caching (and other scoped caching) 24 | - eviction by secondary index 25 | - disabling cache for specific function returns 26 | - tiered caching 27 | - size based eviction that puts limits around more than one function at the time 28 | - cache events 29 | 30 | ## Performance 31 | 32 | - [**Performance**](doc/performance.md) 33 | 34 | ## Adding cache to a function 35 | 36 | **With require `[memento.core :as m][memento.config :as mc]`:** 37 | 38 | Define a function + create new cache + attach cache to a function: 39 | 40 | ```clojure 41 | (m/defmemo my-function 42 | {::m/cache {mc/type mc/caffeine}} 43 | [x] 44 | (* 2 x)) 45 | ``` 46 | 47 | ### **The key parts here**: 48 | - `defmemo` works just like `defn` but wraps the function in a cache 49 | - specify the cache configuration via `:memento.core/cache` keyword in function meta 50 | 51 | Quick reminder, there are two ways to provide metadata when defining functions: `defn` allows a meta 52 | map to be provided before the argument list, or you can add meta to the symbol directly as supported by the reader: 53 | 54 | ```clojure 55 | (m/defmemo ^{::m/cache {mc/type mc/caffeine}} my-function 56 | [x] 57 | (* 2 x)) 58 | ``` 59 | 60 | ### Caching an anonymous function 61 | 62 | You can add cache to a function object (in `clojure.core/memoize` fashion): 63 | 64 | ```clojure 65 | (m/memo (fn [] ...) {mc/type mc/caffeine}) 66 | ``` 67 | 68 | ### Other ways to attach Cache to a function 69 | 70 | [Caches and memoize calls](doc/major.md) 71 | 72 | ## Cache conf(iguration) 73 | 74 | See above: `{mc/type mc/caffeine}` 75 | 76 | The cache conf is an open map of namespaced keywords such as `:memento.core/type`, various cache implementations can 77 | use implementation specific config keywords. 78 | 79 | Learning all the keywords and what they do can be hard. To assist you 80 | there are special conf namespaces provided where conf keywords are defined as vars with docs, 81 | so it's easy so you to see which configuration keys are available and what their function is. It also helps 82 | prevent bugs from typing errors. 83 | 84 | The core properties are defined in `[memento.config :as mc]` namespace. Caffeine specific properties are defined 85 | in `[memento.caffeine.config :as mcc]`. 86 | 87 | Here's a couple of equal ways of writing out you cache configuration meta: 88 | 89 | ```clojure 90 | ; the longest 91 | {:memento.core/cache {:memento.core/type :memento.core/caffeine}} 92 | ; using alias 93 | {::m/cache {::m/type ::m/caffeine}} 94 | ; using memento.config vars - recommended 95 | {mc/cache {mc/type mc/caffeine}} 96 | ``` 97 | 98 | ### Core conf 99 | 100 | The core configuration properties: 101 | 102 | #### mc/type 103 | 104 | Cache implementation type, e.g. caffeine, redis, see the implementation library docs. **Make sure 105 | you load the implementation namespace at some point!**. Caffeine namespace is loaded automatically 106 | when memento.core is loaded. 107 | 108 | #### mc/size< 109 | 110 | Size limit expressed in number of entries or total weight if implementation supports weighted cache entries 111 | 112 | #### mc/ttl 113 | 114 | Entry is invalid after this amount of time has passed since its creation 115 | 116 | It's either a number (of seconds), a pair describing duration e.g. `[10 :m]` for 10 minutes, 117 | see `memento.config/timeunits` for timeunits. 118 | 119 | #### mc/fade 120 | 121 | Entry is invalid after this amount of time has passed since last access, see `mc/ttl` for duration 122 | specification. 123 | 124 | #### mc/key-fn, mc/key-fn* 125 | 126 | Specify a function that will transform the function arg list into the final cache key. Used 127 | to drop function arguments that shouldn't factor into cache tag equality. 128 | 129 | The `key-fn` receives a sequence of arguments, `key-fn*` receives multiple arguments as if it 130 | was the function itself. 131 | 132 | See: [Changing the key for cached tag](doc/key-fn.md) 133 | 134 | #### mc/ret-fn 135 | 136 | A function that is called on every cached function return value. Used for general transformations 137 | of return values. 138 | 139 | #### mc/ret-ex-fn 140 | 141 | A function that is called on every thrown Throwable. Used for general transformations 142 | of thrown exceptions values. 143 | 144 | #### mc/seed 145 | 146 | Initial entries to load in the cache. 147 | 148 | #### mc/initial-capacity 149 | 150 | Cache capacity hint to implementation. 151 | 152 | ## Conf is a value (map) 153 | 154 | Cache conf can get quite involved: 155 | 156 | ```clojure 157 | (ns memento.tryout 158 | (:require [memento.core :as m] 159 | ; general cache conf keys 160 | [memento.config :as mc] 161 | ; caffeine specific cache conf keys 162 | [memento.caffeine.config :as mcc])) 163 | 164 | (def my-weird-cache 165 | "Conf for caffeine cache that caches up to 20 seconds and up to 30 entries, uses weak 166 | references and prints when keys get evicted." 167 | {mc/type mc/caffeine 168 | mc/size< 30 169 | mc/ttl 20 170 | mcc/weak-values true 171 | mcc/removal-listener #(println (apply format "Function %s key %s, value %s got evicted because of %s" %&))}) 172 | 173 | (m/defmemo my-function 174 | {::m/cache my-weird-cache} 175 | [x] (* 2 x)) 176 | ``` 177 | 178 | Seeing as cache conf is a map, I recommend a pattern where you have a namespace in your application that contains vars 179 | with your commonly used cache conf maps and functions that generate slightly parameterized 180 | configuration. E.g. 181 | 182 | ```clojure 183 | (ns my-project.cache 184 | (:require [memento.config :as mc])) 185 | 186 | ;; infinite cache 187 | (def inf-cache {mc/type mc/caffeine}) 188 | 189 | (defn for-seconds [n] (assoc inf-cache mc/ttl n)) 190 | ``` 191 | 192 | Then you just use that in your code: 193 | 194 | ```clojure 195 | (m/defmemo my-function 196 | {::m/cache (cache/for-seconds 60)} 197 | [x] (* x 2)) 198 | ``` 199 | 200 | ## Caches and mount points 201 | 202 | Enabling memoization of a function is composed of two distinct steps: 203 | 204 | - creating a Cache (optional, as you can use an existing cache) 205 | - binding the cache to the function (a MountPoint is used to connect a function being memoized to the cache) 206 | 207 | A cache, an instance of memento.base/Cache, can contain entries from multiple functions and can be shared between memoized functions. 208 | Each memoized function is bound to a Cache via MountPoint. When you call a function such as `(m/as-map a-cached-function)` you are 209 | operating on a MountPoint. 210 | 211 | The reason for this separation is two-fold: 212 | 213 | #### 1. **Improved Size Based Eviction** 214 | 215 | So far all examples implicitly created a new cache for each memoized function, but if we use same cache for multiple 216 | functions, then any size based eviction will apply to them as a whole. If you have 100 memoized functions, and you want to 217 | somewhat limit their memory use, what do you do? In a typical cache library you might limit each of them to 100 entries. So you 218 | allocated 10000 slots total, but one function might have an empty cache, while a very heavily used one needs way more than 100 219 | slots. If all 100 function are backed by same Cache instance with 10000 slots then they automatically balance themselves out. 220 | 221 | #### 2. **Changing cache temporarily to allow for scoped caching** 222 | 223 | This indirection with Mount Points allows us to change which cache is backing a function dynamically. See discussion of tagged 224 | caches below. Here's an example of using tags when caching and scoped caching 225 | 226 | ```clojure 227 | (ns myproject.some-ns 228 | (:require [myproject.cache :as cache] 229 | [memento.core :as m])) 230 | 231 | (defn get-person-by-id [person-id] 232 | (let [person (db/get-person person-id)] 233 | ; tag the returned object with :person + id pair 234 | (m/with-tag-id person :person (:id person)))) 235 | 236 | ; add a cache to the function with tags :person and :request 237 | (m/memo #'get-person-by-id [:person :request] cache/inf) 238 | 239 | ; remove cache entries from every cache tagged :person globally, where the 240 | ; tag is tagged with :person 1 241 | (m/memo-clear-tag! :person 1) 242 | 243 | (m/with-caches :request (constantly (m/create cache/inf)) 244 | ; inside this block, a fresh new cache is used (and discarded) 245 | ; making a scope-like functionality 246 | (get-person-by-id 5)) 247 | ``` 248 | 249 | ## Variable expiry 250 | 251 | Instead of setting a fixed duration of validity for entries in a cache, it is possible 252 | to set these duration on per-tag or per-mount point basis. 253 | 254 | Note that for Caffeine cache variable expiry caching is somewhat slower. 255 | 256 | ### **Read [here](doc/variable-expiry.md)** 257 | 258 | ## Additional features 259 | 260 | #### [Prevent caching of a specific return value (and general return value xform)](doc/ret-fn.md) 261 | #### [Manually add or evict entries](doc/manual-add-remove.md) 262 | 263 | #### `(m/as-map memoized-function)` to get a map of cache entries, also works on MountPoint instances 264 | #### `(m/memoized? a-function)` returns true if the function is memoized 265 | #### `(m/memo-unwrap memoized-function)` returns original uncached function, also works on MountPoint instances 266 | #### `(m/active-cache memoized-function)` returns Cache instance from the function, if present. 267 | 268 | ## Tags 269 | 270 | You can add tags to the caches. Tags enable that you: 271 | 272 | - run actions on caches with specific tags 273 | - **change or update cache of tagged MountPoints within a scope** 274 | - change or update cache of tagged MountPoints permanently 275 | - use secondary index to invalidate entries by a tag + ID pair 276 | 277 | This is a very powerful feature, [read more here.](doc/tags.md) 278 | 279 | ## Loads and invalidations 280 | 281 | Cache only has a single ongoing load for a key going at any one time. For Caffeine cache, if a key is invalidated 282 | during the load, the load is repeated. This is the only way you can get multiple function invocations happen for a single 283 | cached function call. When an tag is invalidated while it's being loaded, the Thread that loads it will be interrupted. 284 | 285 | ## Namespace scan 286 | 287 | You can scan loaded namespaces for annotated vars and automatically create caches. 288 | 289 | [Read more](doc/ns-scan.md) 290 | 291 | ## Events 292 | 293 | You can fire an event at a memoized function. Main use case is to enable adding entries to different functions from same data. 294 | 295 | [Read more](doc/events.md) 296 | 297 | ## Tiered caching 298 | 299 | You can use caches that combine two other caches in some way. The easiest way to generate 300 | the cache configuration needed is to use `memento.core/tiered`,`memento.core/consulting`, `memento.core/daisy`. 301 | 302 | [Read more](doc/tiered.md) 303 | 304 | ## if-cached 305 | 306 | memento.core/if-cache is like an if-let, but the "then" branch executes if the function call 307 | is cached, otherwise else branch is executed. The binding is expected to be a cached function call form, otherwise 308 | an error is thrown. 309 | 310 | Example: 311 | 312 | ```clojure 313 | (if-cached [v (my-function arg1)] 314 | (println "cached value is " v) 315 | (println "value is not cached")) 316 | ``` 317 | 318 | ## Skip/disable caching 319 | 320 | If you set `-Dmemento.enabled=false` JVM option (or change `memento.config/enabled?` var root binding), 321 | then type of all caches created will be `memento.base/no-cache`, which does no caching. 322 | 323 | ## Reload guards 324 | 325 | When you memoize a function with tags, a special object is created that will clean up in internal tag 326 | mappings when memoized function is GCed. It's important when reloading namespaces to remove mount points 327 | on the old function versions. 328 | 329 | It uses finalize, which isn't free (takes extra work to allocate and GC has to work harder), so 330 | if you don't use namespace reloading, and you want to optimize you can disable reload guard objects. 331 | 332 | Set `-Dmemento.reloadable=false` JVM option (or change `memento.config/reload-guards?` var root binding). 333 | 334 | ## Breaking changes 335 | 336 | Patch versions are compatible. Minor version change breaks API for implementation authors, but not for users, 337 | major version change breaks user API. 338 | 339 | Version 1.0.x changed implementation from Guava to Caffeine 340 | Version 0.9.0 introduced many breaking changes. 341 | 342 | ## License 343 | 344 | Copyright © 2020-2021 Rok Lenarčič 345 | 346 | Licensed under the term of the MIT License, see LICENSE. 347 | -------------------------------------------------------------------------------- /test/memento/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns memento.core-test 2 | (:require [clojure.test :refer :all] 3 | [memento.core :as m :refer :all] 4 | [memento.config :as mc] 5 | [memento.caffeine.config :as mcc]) 6 | (:import (java.io IOException) 7 | (memento.base EntryMeta ICache) 8 | (memento.caffeine Expiry) 9 | (memento.mount IMountPoint))) 10 | 11 | (def inf {mc/type mc/caffeine}) 12 | (defn size< [max-size] 13 | (assoc inf mc/size< max-size)) 14 | (defn ret-fn [f] 15 | (assoc inf mc/ret-fn f)) 16 | 17 | (def id (memo identity inf)) 18 | 19 | (defn- check-core-features 20 | [factory] 21 | (let [mine (factory identity) 22 | them (memoize identity)] 23 | (testing "That the memo function works the same as core.memoize" 24 | (are [x y] (= x y) 25 | (mine 42) (them 42) 26 | (mine ()) (them ()) 27 | (mine []) (them []) 28 | (mine #{}) (them #{}) 29 | (mine {}) (them {}) 30 | (mine nil) (them nil))) 31 | (testing "That the memo function has a proper cache" 32 | (is (memoized? mine)) 33 | (is (not (memoized? them))) 34 | (is (= 42 (mine 42))) 35 | (is (not (empty? (into {} (as-map mine))))) 36 | (is (memo-clear! mine)) 37 | (is (empty? (into {} (as-map mine)))))) 38 | (testing "That the cache retries in case of exceptions" 39 | (let [access-count (atom 0) 40 | f (factory 41 | (fn [] 42 | (swap! access-count inc) 43 | (throw (IllegalArgumentException.))))] 44 | (is (thrown? IllegalArgumentException (f))) 45 | (is (thrown? IllegalArgumentException (f))) 46 | (is (= 2 @access-count)))) 47 | (testing "That the memo function does not have a race condition" 48 | (let [access-count (atom 0) 49 | slow-identity 50 | (factory (fn [x] 51 | (swap! access-count inc) 52 | (Thread/sleep 100) 53 | x))] 54 | (every? identity (pvalues (slow-identity 5) (slow-identity 5))) 55 | (is (= @access-count 1)))) 56 | (testing "That exceptions are correctly unwrapped." 57 | (is (thrown? ClassNotFoundException ((factory (fn [] (throw (ClassNotFoundException.))))))) 58 | (is (thrown? IllegalArgumentException ((factory (fn [] (throw (IllegalArgumentException.)))))))) 59 | (testing "Null return caching." 60 | (let [access-count (atom 0) 61 | mine (factory (fn [] (swap! access-count inc) nil))] 62 | (is (nil? (mine))) 63 | (is (nil? (mine))) 64 | (is (= @access-count 1))))) 65 | 66 | (deftest test-memo (check-core-features #(memo % inf))) 67 | 68 | (deftest test-lru 69 | (let [mine (memo identity (size< 2))] 70 | ;; First check that the basic memo behavior holds 71 | (check-core-features #(memo % (size< 2))) 72 | 73 | ;; Now check FIFO-specific behavior 74 | (testing "that when the limit threshold is not breached, the cache works like the basic version" 75 | (are [x y] = 76 | 42 (mine 42) 77 | {[42] 42} (as-map mine) 78 | 43 (mine 43) 79 | {[42] 42, [43] 43} (as-map mine) 80 | 42 (mine 42) 81 | {[42] 42, [43] 43} (as-map mine))) 82 | (testing "that when the limit is breached, the oldest value is dropped" 83 | (are [x y] = 84 | 44 (mine 44) 85 | {[44] 44, [43] 43} (as-map mine))))) 86 | 87 | 88 | (deftest test-ttl 89 | ;; First check that the basic memo behavior holds 90 | (check-core-features #(memo % (assoc inf mc/ttl 2))) 91 | 92 | ;; Now check TTL-specific behavior 93 | (let [mine (memo identity (assoc inf mc/ttl [2 :s]))] 94 | (are [x y] = 95 | 42 (mine 42) 96 | {[42] 42} (as-map mine)) 97 | (Thread/sleep 3000) 98 | (are [x y] = 99 | 43 (mine 43) 100 | {[43] 43} (as-map mine))) 101 | 102 | (let [mine (memo identity (assoc inf mc/ttl [5 :ms])) 103 | limit 2000000 104 | start (System/currentTimeMillis)] 105 | (loop [n 0] 106 | (if-not (mine 42) 107 | (do 108 | (is false (str "Failure on call " n))) 109 | (if (< n limit) 110 | (recur (+ 1 n))))) 111 | (println "ttl test completed" limit "calls in" 112 | (- (System/currentTimeMillis) start) "ms"))) 113 | 114 | (deftest test-memoization-utils 115 | (let [CACHE_IDENTITY (:memento.mount/mount (meta id))] 116 | (testing "that the stored cache is not null" 117 | (is (instance? IMountPoint id))) 118 | (testing "that a populated function looks correct at its inception" 119 | (is (memoized? id)) 120 | (is (instance? ICache (active-cache id))) 121 | (is (as-map id)) 122 | (is (empty? (as-map id)))) 123 | (testing "that a populated function looks correct after some interactions" 124 | ;; Memoize once 125 | (is (= 42 (id 42))) 126 | ;; Now check to see if it looks right. 127 | (is (find (as-map id) '(42))) 128 | (is (= 1 (count (as-map id)))) 129 | ;; Memoize again 130 | (is (= [] (id []))) 131 | (is (find (as-map id) '([]))) 132 | (is (= 2 (count (as-map id)))) 133 | (testing "that upon memoizing again, the cache should not change" 134 | (is (= [] (id []))) 135 | (is (find (as-map id) '([]))) 136 | (is (= 2 (count (as-map id))))) 137 | (testing "if clearing the cache works as expected" 138 | (is (memo-clear! id)) 139 | (is (empty? (as-map id))))) 140 | (testing "that after all manipulations, the cache maintains its identity" 141 | (is (identical? CACHE_IDENTITY (:memento.mount/mount (meta id))))) 142 | (testing "that a cache can be seeded and used normally" 143 | (memo-clear! id) 144 | (is (memo-add! id {[42] 42})) 145 | (is (= 42 (id 42))) 146 | (is (= {[42] 42} (as-map id))) 147 | (is (= 108 (id 108))) 148 | (is (= {[42] 42 [108] 108} (as-map id))) 149 | (is (memo-add! id {[111] nil [nil] 111})) 150 | (is (= 111 (id nil))) 151 | (is (= nil (id 111))) 152 | (is (= {[42] 42 [108] 108 [111] nil [nil] 111} (as-map id)))) 153 | (testing "that we can get back the original function" 154 | (is (memo-clear! id)) 155 | (is (memo-add! id {[42] 24})) 156 | (is (= 24 (id 42))) 157 | (is (= 42 ((memo-unwrap id) 42)))))) 158 | 159 | (deftest memo-with-seed-cmemoize-18 160 | (let [mine (memo identity (assoc inf mc/seed {[42] 99}))] 161 | (testing "that a memo seed works" 162 | (is (= 41 (mine 41))) 163 | (is (= 99 (mine 42))) 164 | (is (= 43 (mine 43))) 165 | (is (= {[41] 41, [42] 99, [43] 43} (as-map mine)))))) 166 | 167 | (deftest memo-with-dropped-args 168 | ;; must use var to preserve metadata 169 | (let [mine (memo + (assoc inf mc/key-fn rest))] 170 | (testing "that key-fnb collapses the cache key space" 171 | (is (= 13 (mine 1 2 10))) 172 | (is (= 13 (mine 10 2 1))) 173 | (is (= 13 (mine 10 2 10))) 174 | (is (= {[2 10] 13, [2 1] 13} (as-map mine)))))) 175 | 176 | (def test-atom (atom 0)) 177 | (defn test-var-fn [x] (swap! test-atom inc) (* x 3)) 178 | 179 | (deftest add-memo-to-var 180 | (testing "that memoing a var works" 181 | (memo #'test-var-fn inf) 182 | (is (= 3 (test-var-fn 1))) 183 | (is (= 3 (test-var-fn 1))) 184 | (is (= 3 (test-var-fn 1))) 185 | (is (= @test-atom 1)))) 186 | 187 | (deftest seed-test 188 | (testing "that seeding a function works" 189 | (let [cached (memo + (assoc inf mc/seed {[3 5] 100 [4 5] 2000}))] 190 | (is (= 50 (cached 20 30))) 191 | (is (= 1 (cached -1 2))) 192 | (is (= 100 (cached 3 5))) 193 | (is (= 2000 (cached 4 5)))))) 194 | 195 | (deftest key-fn-test 196 | (testing "that key-fn works for direct cache" 197 | (let [cached (memo (fn [& ids] ids) (assoc inf mc/key-fn set))] 198 | (is (= [3 2 1] (cached 3 2 1))) 199 | (is (= [3 2 1] (cached 1 2 3))) 200 | (is (= [3 2 1] (cached 1 3 3 2 2 2))) 201 | (is (= [2 1] (cached 2 1)))))) 202 | 203 | (deftest key-fn*-test 204 | (testing "that key-fn works for direct cache" 205 | (let [cached (memo (fn [& ids] ids) (assoc inf mc/key-fn* hash-set))] 206 | (is (= [3 2 1] (cached 3 2 1))) 207 | (is (= [3 2 1] (cached 1 2 3))) 208 | (is (= [3 2 1] (cached 1 3 3 2 2 2))) 209 | (is (= [2 1] (cached 2 1)))))) 210 | 211 | (deftest ret-fn-non-cached 212 | (testing "that ret-fn is ran" 213 | (is (= -4 ((memo + (ret-fn #(* -1 %2))) 2 2))) 214 | (is (= true ((memo (constantly nil) (ret-fn #(nil? %2))) 1))) 215 | (is (= nil ((memo + (ret-fn (constantly nil))) 2 2)))) 216 | (testing "that non-cached is respected" 217 | (let [access-nums (atom []) 218 | f (memo 219 | (fn [number] 220 | (swap! access-nums conj number) 221 | (if (zero? (mod number 3)) (do-not-cache number) number)) 222 | (ret-fn #(if (and (number? %2) (zero? (mod %2 5))) (do-not-cache %2) %2)))] 223 | (is (= (range 20) (map f (range 20)))) 224 | (is (= (range 20) (map f (range 20)))) 225 | (is (= (concat (range 20) [0 3 5 6 9 10 12 15 18]) @access-nums))))) 226 | 227 | (deftest get-tags-test 228 | (testing "tags get returned" 229 | (let [cached (memo identity :person) 230 | cached2 (memo identity [:actor :dog]) 231 | cached3 (memo identity {mc/tags :x})] 232 | (is (= [:person] (tags cached))) 233 | (is (= [:actor :dog] (tags cached2))) 234 | (is (= [:x] (tags cached3)))))) 235 | 236 | (deftest with-caches-test 237 | (testing "a different cache is used within the block" 238 | (let [access-nums (atom []) 239 | f (memo (fn [number] (swap! access-nums conj number)) :person inf)] 240 | (is (= [10] (f 10))) 241 | (is (= [10] (f 10))) 242 | (is (= [10 20] (f 20))) 243 | (is (= [10 20] (f 20))) 244 | (is (= [10 20] @access-nums)) 245 | (with-caches :person (constantly (create inf)) 246 | (is (= [10 20 10] (f 10))) 247 | (is (= [10 20 10] (f 10))) 248 | (is (= [10 20 10 30] (f 30))) 249 | (is (= [10 20 10 30] @access-nums))) 250 | (is (= [10] (f 10))) 251 | (is (= [10 20 10 30 30] (f 30)))))) 252 | 253 | (deftest update-tag-caches-test 254 | (testing "changes cache root binding" 255 | (let [access-nums (atom 0) 256 | f (memo (fn [number] (swap! access-nums + number)) :person inf)] 257 | (is (= 10 (f 10))) 258 | (is (= 10 (f 10))) 259 | (is (= 10 @access-nums)) 260 | (update-tag-caches! :person (constantly (create inf))) 261 | (is (= 20 (f 10))) 262 | (is (= 20 @access-nums)) 263 | (with-caches :person (constantly (create inf)) 264 | (is (= 30 (f 10))) 265 | (is (= 30 (f 10))) 266 | (is (= 30 @access-nums)) 267 | (update-tag-caches! :person (constantly (create inf))) 268 | (is (= 40 (f 10))) 269 | (is (= 40 @access-nums))) 270 | (is (= 20 (f 10))) 271 | (is (= 40 @access-nums)) 272 | (update-tag-caches! :person (constantly (create inf))) 273 | (is (= 50 (f 10))) 274 | (is (= 50 @access-nums))))) 275 | 276 | (deftest tagged-eviction-test 277 | (testing "adding tag ID info" 278 | (is (= (EntryMeta. 1 false #{[:person 55]}) 279 | (-> 1 (with-tag-id :person 55)))) 280 | (is (= (EntryMeta. 1 true #{[:person 55] [:account 6]}) 281 | (-> 1 (with-tag-id :person 55) (with-tag-id :account 6) do-not-cache)))) 282 | (testing "tagged eviction" 283 | (let [f (memo (fn [x] (with-tag-id x :tag x)) :tag inf)] 284 | (is (= {} (as-map f))) 285 | (is (= {[1] 1} (do (f 1) (as-map f)))) 286 | (is (= {[1] 1 [2] 2} (do (f 2) (as-map f)))) 287 | (is (= {[2] 2} (do (memo-clear-tag! :tag 1) (as-map f))))))) 288 | 289 | (deftest fire-event-test 290 | (testing "event is fired on referenced cache" 291 | (let [access-nums (atom 0) 292 | inner-f (fn [x] (swap! access-nums inc) x) 293 | evt-f (fn [this evt] 294 | (m/memo-add! this {[evt] (inc evt)})) 295 | x (m/memo inner-f {mc/type mc/caffeine mc/evt-fn evt-f mc/tags [:a]}) 296 | y (m/memo inner-f {mc/type mc/caffeine mc/evt-fn evt-f})] 297 | (is (= 1 (x 1))) 298 | (is (= 1 (x 1))) 299 | (is (= 1 @access-nums)) 300 | (m/fire-event! x 4) 301 | (m/fire-event! :a 5) 302 | (m/fire-event! y 6) 303 | (is (= {[1] 1 304 | [4] 5 305 | [5] 6} (m/as-map x))) 306 | (is (= {[6] 7} (m/as-map y))) 307 | (is (= 5 (x 4))) 308 | (is (= 6 (x 5))) 309 | (is (= 7 (y 6))) 310 | (is (= 1 @access-nums))))) 311 | 312 | (deftest if-cached-test 313 | (testing "if-cached executes then when cached" 314 | (let [x (m/memo identity {mc/type mc/caffeine})] 315 | (x 2) 316 | (is (= 2 317 | (m/if-cached [y (x 2)] 318 | y 319 | (throw (ex-info "Shouldn't throw" {}))))))) 320 | (testing "if-cached executes else when not cached" 321 | (let [x (m/memo identity {mc/type mc/caffeine})] 322 | (is (= ::none 323 | (m/if-cached [y (x 2)] 324 | (throw (ex-info "Shouldn't throw" {})) 325 | ::none)))))) 326 | 327 | (deftest put-during-load-test 328 | (testing "adding entries during load" 329 | (let [c (m/create inf) 330 | fn1 (m/memo identity {} c) 331 | fn2 (m/memo (fn [x] (m/memo-add! fn1 {[x] (inc x)}) 332 | (dec x)))] 333 | (is (= 4 (fn2 5))) 334 | (is (= 6 (fn1 5)))))) 335 | 336 | (defn fib [x] (if (<= x 1) 1 (+ (fib (- x 2)) (fib (dec x))))) 337 | 338 | (memo #'fib inf) 339 | 340 | (defn recursive [x] (recursive x)) 341 | 342 | (memo #'recursive inf) 343 | 344 | (deftest recursive-test 345 | (testing "recursive loads" 346 | (is (= 20365011074 (fib 50))) 347 | (is (thrown? StackOverflowError (recursive 1))))) 348 | 349 | (deftest concurrent-load 350 | (testing "concurrent test" 351 | (let [cnt (atom 0) 352 | f (m/memo (fn [x] 353 | (Thread/sleep 1000) 354 | (swap! cnt inc) x) 355 | inf) 356 | v (doall (repeatedly 5 #(future (f 1))))] 357 | (is (= [1 1 1 1 1] (mapv deref v)))))) 358 | 359 | (deftest vectors-key-fn* 360 | (testing "vectors don't throw exception when used with key-fn*" 361 | (let [c (m/memo identity (assoc inf mc/key-fn* identity))] 362 | (is (some? (m/memo-add! c {[1] 2})))))) 363 | 364 | (deftest invalidation-during-load-test 365 | (testing "bulk invalidation test" 366 | (let [a (atom 0) 367 | c (m/memo (fn [] (Thread/sleep 300) 368 | (m/with-tag-id (swap! a inc) :xx 1)) 369 | (assoc inf mc/tags :xx))] 370 | (future (Thread/sleep 15) 371 | (m/memo-clear-tag! :xx 1)) 372 | (is (= 2 (c))))) 373 | (testing "Invalidation during load test" 374 | (let [a (atom 0) 375 | after (atom 0) 376 | c (m/memo (fn [] (let [r (swap! a inc)] 377 | (Thread/sleep 300) 378 | [r (swap! after inc)])) inf)] 379 | (future (Thread/sleep 10) 380 | (m/memo-clear! c)) 381 | (is (= [2 1] (c)))))) 382 | 383 | (deftest ret-ex-fn-test 384 | (testing "returns transformed-exception" 385 | (let [e (RuntimeException.) 386 | c (m/memo (fn [] (Thread/sleep 100) 387 | (throw (IOException.))) 388 | (assoc inf mc/ret-ex-fn (fn [_ ee] (when (instance? IOException ee) e)))) 389 | f1 (future (try (c) (catch Exception e e))) 390 | f2 (future (try (c) (catch Exception e e)))] 391 | (is (= e @f1)) 392 | (is (= e @f2))))) 393 | 394 | (deftest variable-expiry-test 395 | (testing "Variable expiry" 396 | (let [c (m/memo 397 | identity 398 | (assoc inf mcc/expiry 399 | (reify Expiry 400 | (ttl [this _ k v] v) 401 | (fade [this _ k v]))))] 402 | (c 1) 403 | (c 2) 404 | (c 3) 405 | (Thread/sleep 1100) 406 | (is (= {'(2) 2 '(3) 3} (m/as-map c))))) 407 | (testing "Variable expiry fade" 408 | (let [c (m/memo 409 | identity 410 | (assoc inf mcc/expiry 411 | (reify Expiry 412 | (ttl [this _ k v] ) 413 | (fade [this _ k v] v))))] 414 | (c 1) 415 | (c 2) 416 | (c 3) 417 | (Thread/sleep 1100) 418 | (is (= {'(2) 2 '(3) 3} (m/as-map c))))) 419 | (testing "variable expiry via meta" 420 | (let [c (m/memo 421 | #(with-meta {} {mc/ttl (long (+ 1 %))}) 422 | (assoc inf mcc/expiry mcc/meta-expiry))] 423 | (c 1) 424 | (c 2) 425 | (c 3) 426 | (Thread/sleep 1100) 427 | (is (= {'(1) {} '(2) {} '(3) {}} (m/as-map c))) 428 | (Thread/sleep 1000) 429 | (is (= {'(2) {} '(3) {}} (m/as-map c)))))) --------------------------------------------------------------------------------
29 | * For each ID we add CacheKey to its HashSet. 30 | * 31 | * @param k 32 | * @param v 33 | */ 34 | public void add(CacheKey k, Object v) { 35 | if (v instanceof EntryMeta) { 36 | EntryMeta e = ((EntryMeta) v); 37 | ISeq s = e.getTagIdents().seq(); 38 | while (s != null) { 39 | Set cacheKeys = lookup.computeIfAbsent(s.first(), key -> new HashSet<>()); 40 | synchronized (cacheKeys) { 41 | cacheKeys.add(new IndexEntry(cacheKeys, k)); 42 | } 43 | s = s.next(); 44 | } 45 | } 46 | } 47 | 48 | public void drainKeys(Object tagId, Consumer onValue) { 49 | Set entries = lookup.remove(tagId); 50 | if (entries != null) { 51 | synchronized (entries) { 52 | for (IndexEntry e : entries) { 53 | CacheKey c = e.get(); 54 | if (c != null) { 55 | onValue.accept(c); 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | 63 | private static final ReferenceQueue evicted = new ReferenceQueue<>(); 64 | 65 | public static class IndexEntry extends WeakReference { 66 | private final Set home; 67 | private final int hash; 68 | 69 | public IndexEntry(Set home, CacheKey key) { 70 | super(key, evicted); 71 | this.hash = key.hashCode(); 72 | this.home = home; 73 | } 74 | 75 | public void delete() { 76 | synchronized (home) { 77 | home.remove(this); 78 | } 79 | } 80 | 81 | @Override 82 | // this is only used when adding entries, so we can expect this to have the underlying key here 83 | public boolean equals(Object o) { 84 | if (this == o) return true; 85 | if (o instanceof IndexEntry) { 86 | IndexEntry that = (IndexEntry) o; 87 | return hash == that.hash && Objects.equals(get(), that.get()); 88 | } else { 89 | return false; 90 | } 91 | } 92 | 93 | @Override 94 | // this is special hashcode, it remembers the key object's hash so we can kinda use hashset 95 | public int hashCode() { 96 | return hash; 97 | } 98 | } 99 | 100 | public static class Cleaner implements Runnable { 101 | 102 | @Override 103 | public void run() { 104 | while (true) { 105 | try { 106 | IndexEntry ref = (IndexEntry) evicted.remove(); 107 | ref.delete(); 108 | } catch (InterruptedException e) { 109 | // 110 | } 111 | } 112 | } 113 | } 114 | 115 | public static final Thread cleanerThread = new Thread(new Cleaner(), "Memento Secondary Index Cleaner"); 116 | 117 | static { 118 | cleanerThread.setDaemon(true); 119 | cleanerThread.start(); 120 | } 121 | 122 | 123 | } 124 | -------------------------------------------------------------------------------- /doc/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | You can fire an event at a memoized function. The target can be a particular function (or MountPoint), or 3 | you can specify a tag (and all tagged functions get the event). Each function can configure its own handler for 4 | events. Event can be any object, I suggest you use a structure that will enable event handlers to distinguish 5 | events. 6 | 7 | Event handler is a function of two arguments, the MountPoint it's been triggered (most core functions work on those) 8 | and the event. 9 | 10 | Main use case is to enable adding entries to different functions from same data. Example: 11 | 12 | ```clojure 13 | (defn get-project-name 14 | "Returns project name" 15 | [project-id]) 16 | 17 | (m/memo #'get-project-name inf) 18 | 19 | (defn get-project-owner 20 | "Returns project's owner user ID" 21 | [project-id]) 22 | 23 | (m/memo #'get-project-owner inf) 24 | 25 | (defn get-user-projects 26 | "Returns a big expensive list" 27 | [user-id] 28 | (let [project-list '...] 29 | project-list)) 30 | ``` 31 | 32 | In that example, when `get-user-projects` is called, we might load over a 100 projects, and we'd hate to waste that 33 | and not inform `get-project-name` and `get-project-owner` about the facts we've established here, especially since we 34 | might be calling these smaller functions in a loop right after fetching the big list. 35 | 36 | Here's a way to make sure data is reused by manually pushing entries into the caches as supported by most caching libs: 37 | 38 | ```clojure 39 | (defn get-user-projects 40 | "Returns a big expensive list" 41 | [user-id] 42 | (let [project-list '...] 43 | ;; preload entries for seen projects into caches 44 | (m/memo-add! get-project-name 45 | (zipmap (map (comp list :id) project-list) 46 | (map :name project-list))) 47 | (m/memo-add! get-project-owner 48 | (zipmap (map (comp list :id) project-list) 49 | (repeat user-id))) 50 | project-list)) 51 | ``` 52 | 53 | The problem with this solution is that it is an absolute nightmare to maintain: 54 | - adding/removing data consuming functions like `get-project-name` means that I have to also fix producing 55 | functions like `get-user-projects` 56 | - worse yet, the producer function has to be aware of what the argument list of consuming function looks like 57 | and how the output of that function is related to that. For instance if I change arg list for `get-project-owner` 58 | I must fix the `get-user-projects` code that pushes cache entries 59 | - if I want additional producers like `get-user-projects` then each such producer must implement all these changes 60 | and each has a massive block to feed all the consumers 61 | 62 | 63 | I can use events instead and co-locate the code that feeds the cache with the function: 64 | 65 | ```clojure 66 | (defn get-project-name 67 | "Returns project name" 68 | [project-id]) 69 | 70 | (m/memo #'get-project-name 71 | (assoc inf 72 | mc/evt-fn (m/evt-cache-add 73 | :project-seen 74 | (fn [{:keys [name id]}] {[id] name})) 75 | mc/tags [:project])) 76 | 77 | (defn get-project-owner 78 | "Returns project's owner user ID" 79 | [project-id]) 80 | 81 | (m/memo #'get-project-owner 82 | (assoc inf 83 | mc/evt-fn (m/evt-cache-add 84 | :project-seen 85 | (fn [{:keys [id user-id]}] {[id] user-id})) 86 | mc/tags [:project])) 87 | 88 | (defn get-user-projects 89 | "Returns a big expensive list" 90 | [user-id] 91 | (let [project-list '...] 92 | (doseq [p project-list] 93 | (m/fire-event! :project [:project-seen (assoc p :user-id user-id)])) 94 | project-list)) 95 | ``` 96 | 97 | We're using the `evt-cache-add` convenience function that assumes event shape is a 98 | vector of type + payload and that the intent is to add entries to the cache. 99 | 100 | In this case the producer function is only concerned with firing events at tagged caches. 101 | It doesn't need to consider the number of shape of consumers. 102 | 103 | The caching declaration of consumer functions is where there the cache feeding logic is located, 104 | which makes things manageable. 105 | -------------------------------------------------------------------------------- /doc/tags.md: -------------------------------------------------------------------------------- 1 | # Tags 2 | 3 | You can add tags to the caches. You can run actions on caches with specific tags. 4 | 5 | You can specify them via `:memento.core/tags` key (also `mc/tags` value), 6 | or you can simply specify them instead of conf map, which creates a tagged cache 7 | of noop type (that you can replace later). 8 | 9 | ```clojure 10 | (m/memo {mc/tags [:request-scope :person]} #'get-person-by-id) 11 | (m/memo [:request-scope :person] #'get-person-by-id) 12 | (m/memo :person #'get-person-by-id) 13 | ``` 14 | 15 | #### Utility 16 | 17 | You can fetch tags on a memoized function. 18 | 19 | ```clojure 20 | (m/tags get-person-by-id) 21 | => [:person] 22 | ``` 23 | 24 | You can fetch all mount points of functions that are tagged by a specific tag: 25 | 26 | ```clojure 27 | (m/mounts-by-tag :person) 28 | => #{#memento.mount.TaggedMountPoint{...}} 29 | ``` 30 | 31 | #### Change / update cache within a scope 32 | 33 | ```clojure 34 | (m/with-caches :person (constantly (m/create cache/inf-cache)) 35 | (get-person-by-id db-spec 1 12) 36 | (get-person-by-id db-spec 1 12) 37 | (get-person-by-id db-spec 1 12)) 38 | ``` 39 | 40 | Every memoized function (mountpoint) inside the block has its cache updated to the result of the 41 | provided function. In this example, all the `:person` tagged functions will use the same unbounded cache 42 | within the block. This effectively stops them from using any previously cached values and any values added to 43 | cache are dropped when block is exited. 44 | 45 | **This is extremely useful to achieve request scoped caching.** 46 | 47 | #### Updating / changing cache instance permanently 48 | 49 | You can update Cache instances of all functions tagged by a specific tag. This will modify root binding 50 | if not inside `with-caches`, otherwise it will modify the binding. 51 | 52 | ```clojure 53 | (m/update-tag-caches! :person (constantly (m/create cache/inf-cache))) 54 | ``` 55 | 56 | All `:person` tagged memoized functions will from this point on use a new empty unbounded cache. 57 | 58 | #### Applying operations to tagged memoized functions 59 | 60 | Use `mounts-by-tag` to grab mount points and then apply any of the core functions to them. 61 | 62 | ```clojure 63 | (doseq [f (m/mounts-by-tag :person)] 64 | (m/memo-clear! f)) 65 | ``` 66 | 67 | #### Invalidate entries by a tag + ID combo 68 | 69 | You can add tag + ID pairs to cached values. This can be later used to invalidate these 70 | entried based on that ID. 71 | 72 | ID can be a number like `1` or something complex like a `[1 {:region :us}]`. You can attach multiple 73 | IDs for same tag. 74 | 75 | You can add the tag ID pair inside the cached function or in the ret-fn: 76 | 77 | ```clojure 78 | (defn get-person-by-id [db-conn account-id person-id] 79 | (if (nil? person-id) 80 | {:status 404} 81 | (-> {:status 200} 82 | (m/with-tag-id :person person-id) 83 | (m/with-tag-id :account account-id)))) 84 | 85 | (m/memo #'get-person-by-id [:person :account] cache/inf-cache) 86 | ``` 87 | 88 | Now you can invalidate all entries linked to a specified ID in any correctly tagged cache: 89 | 90 | ```clojure 91 | (m/memo-clear-tag! :account 1) 92 | ``` 93 | 94 | This will invalidate entries with tag id `:account, 1` in all `:account` tagged functions. 95 | 96 | As mentioned, you can move code that adds the id information to a `ret-fn`: 97 | 98 | ```clojure 99 | ; first argument is args, second is the returned value 100 | (defn ret-fn [[_ account-id person-id :as args] resp] 101 | (if (<= 400 (:status resp) 599) 102 | (m/do-not-cache resp) 103 | (-> resp 104 | ; we can grab the data from arg list 105 | (m/with-tag-id :account account-id) 106 | (m/with-tag-id :person person-id) 107 | ; or we can grab it from the return value 108 | (m/with-tag-id :person (:id resp))))) 109 | 110 | (defn get-person-by-id [db-conn account-id person-id] 111 | (if (nil? person-id) 112 | {:status 404} 113 | {:status 200 :id person-id :name ....})) 114 | 115 | (m/memo #'get-person-by-id [:person :account] (assoc cache/inf-cache mc/ret-fn ret-fn)) 116 | ``` 117 | 118 | Later you can invalidate tagged entries: 119 | 120 | ```clojure 121 | (m/memo-clear-tag! :person 1) 122 | 123 | ;; get better atomicity with bulk operation 124 | 125 | (m/memo-clear-tags! [:person 1] [:user 33]) 126 | ``` 127 | 128 | ## Invalidation atomicity 129 | 130 | ``` 131 | 132 | ``` 133 | -------------------------------------------------------------------------------- /java/memento/base/LockoutMap.java: -------------------------------------------------------------------------------- 1 | package memento.base; 2 | 3 | import clojure.lang.IPersistentSet; 4 | import clojure.lang.ISeq; 5 | import clojure.lang.ITransientMap; 6 | import clojure.lang.PersistentHashMap; 7 | 8 | import java.util.concurrent.CopyOnWriteArraySet; 9 | import java.util.concurrent.atomic.AtomicReference; 10 | 11 | /** 12 | * This class represents a global map of ongoing bulk invalidations of Tag Ids. Await lockout can be used 13 | * to await for bulk invalidation to finish. Adding listeners is used to enable implementations to 14 | * be able to communicate these lockouts outside the JVM. 15 | */ 16 | public class LockoutMap { 17 | 18 | public static LockoutMap INSTANCE = new LockoutMap(); 19 | 20 | private final AtomicReference m = new AtomicReference<>(PersistentHashMap.EMPTY); 21 | 22 | public LockoutMap() { 23 | 24 | } 25 | 26 | private final CopyOnWriteArraySet listeners = new CopyOnWriteArraySet<>(); 27 | 28 | public void addListener(Listener l) { 29 | listeners.add(l); 30 | } 31 | 32 | /** 33 | * Add a new lockout, returning the Latch, (if newer than existing) for the given keys 34 | * 35 | * @param tagsAndIds 36 | * @return 37 | */ 38 | public void startLockout(Iterable tagsAndIds, LockoutTag tag) { 39 | PersistentHashMap oldMap; 40 | PersistentHashMap newv; 41 | do { 42 | oldMap = m.get(); 43 | ITransientMap newMap = oldMap.asTransient(); 44 | for (Object e : tagsAndIds) { 45 | newMap.assoc(e, tag); 46 | } 47 | newv = (PersistentHashMap) newMap.persistent(); 48 | } while (!m.compareAndSet(oldMap, newv)); 49 | listeners.forEach(l -> l.startLockout(tagsAndIds, tag)); 50 | } 51 | 52 | /** 53 | * End lockout for keys and the marker. After map is updated, marker's latch is released 54 | * 55 | * @param tagsAndIds 56 | * @param tag 57 | */ 58 | public void endLockout(Iterable tagsAndIds, LockoutTag tag) { 59 | PersistentHashMap oldMap; 60 | PersistentHashMap newv; 61 | do { 62 | oldMap = m.get(); 63 | ITransientMap newMap = oldMap.asTransient(); 64 | for (Object e : tagsAndIds) { 65 | if (oldMap.get(e) == tag) { 66 | newMap.without(e); 67 | } 68 | } 69 | newv = (PersistentHashMap) newMap.persistent(); 70 | } while (!m.compareAndSet(oldMap, newv)); 71 | try { 72 | listeners.forEach(l -> l.endLockout(tagsAndIds, tag)); 73 | } finally { 74 | tag.getLatch().countDown(); 75 | } 76 | } 77 | 78 | private static boolean awaitMarker(PersistentHashMap lockouts, Object obj) throws InterruptedException { 79 | LockoutTag lockoutTag = (LockoutTag) lockouts.get(obj); 80 | if (lockoutTag != null) { 81 | lockoutTag.getLatch().await(); 82 | return true; 83 | } else { 84 | return false; 85 | } 86 | } 87 | 88 | /** 89 | * It awaits an invalidations to finish, returns after that. Returns true if the entry 90 | * was invalid and an invalidation was awaited. 91 | */ 92 | public static boolean awaitLockout(Object promiseValue) throws InterruptedException { 93 | if (promiseValue instanceof EntryMeta) { 94 | IPersistentSet idents = ((EntryMeta) promiseValue).getTagIdents(); 95 | if (idents.count() != 0) { 96 | PersistentHashMap invalidations = LockoutMap.INSTANCE.m.get(); 97 | if (invalidations.isEmpty()) { 98 | return false; 99 | } 100 | ISeq identSeq = ((EntryMeta) promiseValue).getTagIdents().seq(); 101 | boolean ret = false; 102 | while (identSeq != null) { 103 | ret |= awaitMarker(invalidations, identSeq.first()); 104 | identSeq = identSeq.next(); 105 | } 106 | return ret; 107 | } 108 | } 109 | return false; 110 | } 111 | 112 | public interface Listener { 113 | void startLockout(Iterable tagsAndIds, LockoutTag latch); 114 | 115 | void endLockout(Iterable tagsAndIds, LockoutTag latch); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/memento/caffeine/config.clj: -------------------------------------------------------------------------------- 1 | (ns memento.caffeine.config 2 | "Caffeine implementation config helpers. 3 | 4 | Contains documented definitions of standard options of Caffeine cache config." 5 | {:author "Rok Lenarčič"} 6 | (:import (memento.caffeine Expiry))) 7 | 8 | (def removal-listener 9 | "Cache setting, corresponds to .removalListener on Caffeine. 10 | 11 | A function of four arguments (fn [f key value removal-cause] nil), 12 | that will be called whenever an entry is removed. 13 | 14 | The four arguments are: 15 | 16 | - the function being cached 17 | - the key (arg-list transformed by key-fn if any) 18 | - the value (after ret-fn being applied) 19 | - com.github.benmanes.caffeine.cache.RemovalCause 20 | 21 | Warning: any exception thrown by listener will not be propagated to the Cache user, only logged via a Logger." 22 | :memento.caffeine/removal-listener) 23 | 24 | (def weight< 25 | "Cache setting, a long. 26 | 27 | Specifies the maximum weight of entries the cache may contain. If using this option, 28 | you must provide `kw-weight` option for cache to calculate the weight of entries. 29 | 30 | A cache may evict entries before the specified limit is reached." 31 | :memento.caffeine/weight<) 32 | 33 | (def kv-weight 34 | "Cache setting, a function of 3 arguments (fn [f key value] int-weight), 35 | that will be used to determine the weight of entries. 36 | 37 | It should return an int, the weight of the entry. 38 | 39 | The 3 arguments are: 40 | - the first argument is the function being cached 41 | - the second argument is the key (arg-list transformed by key-fn if any) 42 | - the third argument is the value (after ret-fn being applied)" 43 | :memento.caffeine/kv-weight) 44 | 45 | ;; makes no sense, since user cannot hold on to our CacheKey instances 46 | #_(def weak-keys 47 | "Cache setting, corresponds to .weakKeys on CacheBuilder. 48 | 49 | Boolean flag, enabling storing keys using weak references. 50 | 51 | Specifies that each key (not value) stored in the cache should be wrapped in a WeakReference 52 | (by default, strong references are used). 53 | 54 | Warning: when this method is used, the resulting cache will use identity (==) comparison 55 | to determine equality of keys. Its Cache.asMap() view will therefore technically violate 56 | the Map specification (in the same way that IdentityHashMap does). 57 | 58 | The identity comparison makes this not very useful." 59 | :memento.caffeine/weak-keys) 60 | 61 | (def weak-values 62 | "Cache setting, corresponds to .weakValues on CacheBuilder. 63 | 64 | Boolean flag, enabling storing values using weak references. 65 | 66 | This allows entries to be garbage-collected if there are no other (strong or soft) references to the values." 67 | :memento.caffeine/weak-values) 68 | 69 | (def soft-values 70 | "Cache setting, corresponds to .softValues on CacheBuilder. 71 | 72 | Boolean flag, enabling storing values using soft references. 73 | 74 | Softly referenced objects are garbage-collected in a globally least-recently-used manner, 75 | in response to memory demand. Because of the performance implications of using soft references, 76 | we generally recommend using the more predictable maximum cache size instead." 77 | :memento.caffeine/soft-values) 78 | 79 | (def stats 80 | "Cache setting, boolean flag, enabling collection of stats. 81 | 82 | Corresponds to .enableStats on CacheBuilder. 83 | 84 | You can retrieve a cache's stats by using memento.caffeine/stats. 85 | 86 | Returns com.google.common.cache.CacheStats instance or nil." 87 | :memento.caffeine/stats) 88 | 89 | (def ticker 90 | "Cache setting, corresponds to .ticker on CacheBuilder. 91 | 92 | A function of zero arguments that should return current nano time. 93 | This is used when doing time based eviction. 94 | 95 | The default is (fn [] (System/nanoTime)). 96 | 97 | This is useful for testing and you can also make the time move in discrete amounts (e.g. you can 98 | make all cache accesses in a request have same time w.r.t. eviction)." 99 | :memento.caffeine/ticker) 100 | 101 | (def expiry 102 | "A cache and function bind setting, an instance of memento.caffeine.Expiry interface. 103 | 104 | Enables user to specify cached entry expiry on each entry individually. If interface 105 | functions return nil, then ttl and fade settings apply." 106 | :memento.caffeine/expiry) 107 | 108 | (def meta-expiry 109 | "A memento.caffeine.Expiry instance that looks for fade and ttl keys on object metas and uses those to control 110 | variable expiry." 111 | Expiry/META_VAL_EXP) -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.68 4 | 5 | - correctly wraps MultiFn 6 | 7 | ## 2.0.65 8 | 9 | - added a check that will throw an error if encountering mc/cache key in wrong place, cache configuration 10 | 11 | ## 2.0.63 12 | 13 | - upgrade from Caffeine 2 to Caffeine 3. Min Java changed from 8 to 11. 14 | 15 | ## 1.4.62 16 | 17 | - remove unneeded operation when adding entries into the map 18 | 19 | ## 1.4.61 20 | 21 | - when an tag is invalidated during load, the loading thread will be interrupted 22 | 23 | ## 1.3.60 24 | 25 | - improved variable expiry 26 | - added stronger prevention of the use of invalid entries 27 | 28 | ## 1.2.59 29 | 30 | - added variable expiry option (see README) 31 | - removed some reflection 32 | - enabled weakValues as an option for caffeine cache 33 | - removed weakKeys as it's not possible to use that option 34 | 35 | ## 1.2.58 36 | 37 | - added ret-ex-fn option to transform exceptions being thrown by cache in the same way ret-fn works for values 38 | 39 | ## 1.2.57 40 | 41 | - add predicate memento.core/none-cache? that checks if cache is of none type 42 | - new function memento.core/caches-by-tag 43 | - important improvement of atomicity for invalidations by tag or function 44 | - important fix for thread synchronization when adding tagged entries 45 | - important fix for secondary indexes clearing 46 | - reduced memory use 47 | - improving performance on evictions when an eviction listener isn't used 48 | - *BREAKING CHANGE FOR IMPLEMENTATIONS* `invalidateId` is now `invalidateIds` and takes an iterable of tag ids, the implementations are expected to take care to block loads until invalidations are complete. Use the `memento.base/lockout-map` for this purpose. 49 | 50 | ## 1.1.54 51 | 52 | - Improve handling of Vectors when adding entries 53 | 54 | ## 1.1.53 55 | 56 | - improve memory use 57 | 58 | ## 1.1.52 59 | 60 | - fixes a bug with concurrent loads causing some of them to return nil as a result 61 | 62 | ## 1.1.51 63 | 64 | - DO NOT USE 65 | - add check for cyclical loads to caffeine cache, e.g. cached function calling itself with same parameters, this now throws StackOverflowError, which is the error you'd get in this situation with uncached function 66 | - improved performance 67 | 68 | ## 1.1.50 69 | 70 | - added option of using SoftReferences in caffeine cache 71 | - fixed reload-guards? var not being redefinable 72 | 73 | ## 1.1.45 74 | 75 | - add getters/setters to MultiCache for the delegate/upstream cache, also add clojure functions to access these properties to `memento.multi` namespace 76 | - moved CacheKey to memento.base Java package 77 | 78 | ## 1.1.44 79 | 80 | - fix bug which would have the cache return nil when concurrently accessing a value being calculated that ends being uncacheable 81 | 82 | ## 1.1.42 83 | - big internal changes now uses Java objects for most things for smaller memory profile and smaller callstack 84 | - significant improvements to callstack size for cached call 85 | - **This is breaking changes for any implementation, shouldn't affect users** 86 | - Fixes issue where namespace scan would stack caches on the same function over and over if called multiple times 87 | 88 | Here is an example of previous callstack of recursively cached call (without ret-fn): 89 | ```clojure 90 | at myns$myfn.invoke(myns.clj:12) 91 | at clojure.lang.AFn.applyToHelper(AFn.java:160) 92 | at clojure.lang.AFn.applyTo(AFn.java:144) 93 | at clojure.core$apply.invokeStatic(core.clj:667) 94 | at clojure.core$apply.invoke(core.clj:662) 95 | at memento.caffeine.CaffeineCache$fn__2536.invoke(caffeine.clj:122) 96 | at memento.caffeine.CaffeineCache.cached(caffeine.clj:121) 97 | at memento.mount.UntaggedMountPoint.cached(mount.clj:50) 98 | at memento.mount$bind$fn__2432.doInvoke(mount.clj:119) 99 | at clojure.lang.RestFn.applyTo(RestFn.java:137) 100 | at clojure.lang.AFunction$1.doInvoke(AFunction.java:31) 101 | at clojure.lang.RestFn.invoke(RestFn.java:436) 102 | at myns$myfn.invokeStatic(myns.clj:17) 103 | at myns$myfn.invoke(myns.clj:12) 104 | ``` 105 | 106 | And callstack after: 107 | ```clojure 108 | at myns$myfn.invoke(myns.clj:12) 109 | at clojure.lang.AFn.applyToHelper(AFn.java:160) 110 | at memento.caffeine.CaffeineCache$fn__2052.invoke(caffeine.clj:120) 111 | at memento.caffeine.CaffeineCache.cached(caffeine.clj:119) 112 | at memento.mount.CachedFn.invoke(CachedFn.java:110) 113 | at myns$myfn.invokeStatic(myns.clj:17) 114 | at myns$myfn.invoke(myns.clj:12) 115 | ``` 116 | 117 | From 11 stack frames to 4. 118 | 119 | ## 1.0.37 120 | - remove Guava and replace with Caffeine 121 | - rewrite the readme 122 | - mark Guava namespaces for deprecation 123 | 124 | ## 0.9.36 (2022-03-01) 125 | - add `memento.core/defmemo` 126 | 127 | ## 0.9.35 (2022-02-22) 128 | - add `memento.core/key-fn*` mount setting 129 | 130 | ## 0.9.34 (2022-02-16) 131 | - add support for using meta 132 | 133 | ## 0.9.3 (2021-05-08) 134 | ### Features added 135 | - `memento.core/if-config` 136 | -------------------------------------------------------------------------------- /java/memento/caffeine/SpecialPromise.java: -------------------------------------------------------------------------------- 1 | package memento.caffeine; 2 | 3 | import clojure.lang.ISeq; 4 | import memento.base.EntryMeta; 5 | import memento.base.LockoutMap; 6 | 7 | import java.util.HashSet; 8 | import java.util.concurrent.CountDownLatch; 9 | 10 | /** 11 | * Special promise for use in indirection in Caffeine cache. Do not use otherwise. 12 | * 13 | * This class is NOT threadsafe. The intended use is as a faster CompletableFuture that is 14 | * aware of the thread that made it, so it can detect when same thread is trying to await on it 15 | * and throw to prevent deadlock. 16 | * 17 | * It is also expected that it is created and delivered by the same thread and it is expected that 18 | * any other thread is awaiting result and that only a single attempt is made at delivering the result. 19 | * 20 | * It does not include logic to deal with multiple calls to deliver the promise, as it's optimized for 21 | * a specific use case. 22 | */ 23 | public class SpecialPromise { 24 | 25 | private static final AltResult NIL = new AltResult(null); 26 | private final CountDownLatch d = new CountDownLatch(1); 27 | // these 2 don't need to be thread-safe, because they are only used to check 28 | // if current thread is one that created and started the load on the promise 29 | // so even with non-volatile, check is only true if thread is same as current thread 30 | // so no memory barrier needed 31 | private final HashSet invalidatedIds = new HashSet<>(); 32 | private volatile Thread thread; 33 | private volatile Object result; 34 | 35 | public void init() { 36 | this.thread = Thread.currentThread(); 37 | } 38 | 39 | public Object await(Object stackOverflowContext) throws Throwable { 40 | if (thread == Thread.currentThread()) { 41 | throw new StackOverflowError("Recursive load on key: " + stackOverflowContext); 42 | } 43 | Object r; 44 | if ((r = result) == null) { 45 | d.await(); 46 | r = result; 47 | } 48 | if (r instanceof AltResult) { 49 | Throwable x = ((AltResult) r).value; 50 | if (x == null) { 51 | return null; 52 | } else { 53 | throw x; 54 | } 55 | } else { 56 | return r; 57 | } 58 | } 59 | 60 | private boolean isLockedOut(EntryMeta em) { 61 | try { 62 | return LockoutMap.awaitLockout(em); 63 | } catch (InterruptedException e) { 64 | return true; 65 | } 66 | } 67 | 68 | // Returns true if delivered object is viable 69 | public boolean deliver(Object r) { 70 | if (r instanceof EntryMeta) { 71 | EntryMeta em = (EntryMeta) r; 72 | if (isLockedOut(em) || hasInvalidatedTagId(em)) { 73 | result = EntryMeta.absent; 74 | return false; 75 | } 76 | } 77 | if (result != EntryMeta.absent) { 78 | result = r == null ? NIL : r; 79 | return true; 80 | } 81 | return false; 82 | } 83 | 84 | public void deliverException(Throwable t) { 85 | result = new AltResult(t); 86 | } 87 | 88 | public Object getNow() throws Throwable { 89 | Object r; 90 | if (d.getCount() != 0) { 91 | return EntryMeta.absent; 92 | } 93 | if ((r = result) instanceof AltResult) { 94 | Throwable x = ((AltResult) r).value; 95 | if (x == null) { 96 | return null; 97 | } else { 98 | throw x; 99 | } 100 | } else { 101 | return r == null ? EntryMeta.absent : r; 102 | } 103 | } 104 | 105 | public void invalidate() { 106 | result = EntryMeta.absent; 107 | thread.interrupt(); 108 | } 109 | 110 | public boolean isInvalid() { 111 | return result == EntryMeta.absent; 112 | } 113 | 114 | public void releaseResult() { 115 | d.countDown(); 116 | } 117 | 118 | private boolean hasInvalidatedTagId(EntryMeta entryMeta) { 119 | synchronized (invalidatedIds) { 120 | ISeq s = entryMeta.getTagIdents().seq(); 121 | while (s != null) { 122 | if (invalidatedIds.contains(s.first())) { 123 | return true; 124 | } 125 | s = s.next(); 126 | } 127 | } 128 | return false; 129 | } 130 | 131 | public void addInvalidIds(Iterable ids) { 132 | synchronized (invalidatedIds) { 133 | for (Object id : ids) { 134 | invalidatedIds.add(id); 135 | } 136 | } 137 | } 138 | 139 | private static class AltResult { 140 | Throwable value; 141 | 142 | public AltResult(Throwable value) { 143 | this.value = value; 144 | } 145 | } 146 | 147 | } -------------------------------------------------------------------------------- /src/memento/caffeine.clj: -------------------------------------------------------------------------------- 1 | (ns memento.caffeine 2 | "Caffeine cache implementation." 3 | {:author "Rok Lenarčič"} 4 | (:require [memento.base :as b]) 5 | (:import (java.util.concurrent TimeUnit) 6 | (memento.base Durations CacheKey EntryMeta ICache Segment) 7 | (com.github.benmanes.caffeine.cache Caffeine Weigher Ticker) 8 | (memento.caffeine CaffeineCache_ SecondaryIndex SpecialPromise Expiry) 9 | (memento.mount IMountPoint))) 10 | 11 | (defn create-expiry 12 | "Assumes variable expiry is needed. So either ttl or fade is a function." 13 | [ttl fade ^Expiry cache-expiry] 14 | (let [read-default (some-> (or ttl fade) (Durations/nanos)) 15 | write-default (Durations/nanos (or ttl fade [Long/MAX_VALUE :ns]))] 16 | (reify com.github.benmanes.caffeine.cache.Expiry 17 | (expireAfterCreate [this k v current-time] 18 | (if (instance? SpecialPromise v) 19 | Long/MAX_VALUE 20 | (.expireAfterUpdate this k v current-time Long/MAX_VALUE))) 21 | (expireAfterUpdate [this k v current-time current-duration] 22 | (if-let [ret (.ttl cache-expiry {} (.getArgs ^CacheKey k) v)] 23 | (Durations/nanos ret) 24 | (if-let [ret (.fade cache-expiry {} (.getArgs ^CacheKey k) v)] 25 | (Durations/nanos ret) 26 | write-default))) 27 | (expireAfterRead [this k v current-time current-duration] 28 | (if (instance? SpecialPromise v) 29 | current-duration 30 | ;; if fade is not specified, keep current validity (probably set by ttl) 31 | (if-let [ret (.fade cache-expiry {} (.getArgs ^CacheKey k) v)] 32 | (Durations/nanos ret) 33 | (or read-default current-duration))))))) 34 | 35 | (defn conf->sec-index 36 | "Creates secondary index for evictions" 37 | [{:memento.core/keys [concurrency]}] 38 | (SecondaryIndex. (or concurrency 4))) 39 | 40 | (defn ^Caffeine conf->builder 41 | "Creates and configures common parameters on the builder." 42 | [{:memento.core/keys [initial-capacity size< ttl fade] 43 | :memento.caffeine/keys [weight< removal-listener kv-weight weak-keys weak-values 44 | soft-values refresh stats ticker expiry]}] 45 | (cond-> (Caffeine/newBuilder) 46 | removal-listener (.removalListener (CaffeineCache_/listener removal-listener)) 47 | initial-capacity (.initialCapacity initial-capacity) 48 | weight< (.maximumWeight weight<) 49 | size< (.maximumSize size<) 50 | kv-weight (.weigher 51 | (reify Weigher (weigh [_this k v] 52 | (kv-weight (.getId ^CacheKey k) 53 | (.getArgs ^CacheKey k) 54 | (b/unwrap-meta v))))) 55 | ;; these don't make sense as the caller cannot hold the CacheKey 56 | ;;weak-keys (.weakKeys) 57 | ;; careful around EntryMeta objects 58 | ;; mean that cached values have another wrapper yet again 59 | weak-values (.weakValues) 60 | soft-values (.softValues) 61 | expiry (.expireAfter (create-expiry ttl fade expiry)) 62 | (and (not expiry) ttl) (.expireAfterWrite (Durations/nanos ttl) TimeUnit/NANOSECONDS) 63 | (and (not expiry) fade) (.expireAfterAccess (Durations/nanos fade) TimeUnit/NANOSECONDS) 64 | ;; not currently used because we don't build a loading cache 65 | refresh (.refreshAfterWrite (Durations/nanos refresh) TimeUnit/NANOSECONDS) 66 | ticker (.ticker (proxy [Ticker] [] (read [] (ticker)))) 67 | stats (.recordStats))) 68 | 69 | (defn assoc-imm-val! 70 | "If cached value is a completable future with immediately available value, assoc it to transient." 71 | [transient-m k v xf] 72 | (let [cv (if (instance? SpecialPromise v) 73 | (.getNow ^SpecialPromise v) 74 | v)] 75 | (if (identical? cv b/absent) 76 | transient-m 77 | (assoc! transient-m k (xf cv))))) 78 | 79 | ;;;;;;;;;;;;;; 80 | ; ->key-fn take 2 args f and key 81 | (defrecord CaffeineCache [conf ^CaffeineCache_ caffeine-cache] 82 | ICache 83 | (conf [this] conf) 84 | (cached [this segment args] 85 | (.cached caffeine-cache segment args)) 86 | (ifCached [this segment args] 87 | (.ifCached caffeine-cache segment args)) 88 | (invalidate [this segment] 89 | (.invalidate caffeine-cache ^Segment segment) 90 | this) 91 | (invalidate [this segment args] (.invalidate caffeine-cache ^Segment segment args) 92 | this) 93 | (invalidateAll [this] (.invalidateAll caffeine-cache) this) 94 | (invalidateIds [this ids] 95 | (.invalidateIds caffeine-cache ids) 96 | this) 97 | (addEntries [this segment args-to-vals] 98 | (.addEntries caffeine-cache segment args-to-vals) 99 | this) 100 | (asMap [this] (persistent! 101 | (reduce (fn [m [k v]] (assoc-imm-val! m k v b/unwrap-meta)) 102 | (transient {}) 103 | (.asMap caffeine-cache)))) 104 | (asMap [this segment] 105 | (persistent! 106 | (reduce (fn [m [^CacheKey k v]] 107 | (if (= (.getId segment) (.getId k)) (assoc-imm-val! m (.getArgs k) v b/unwrap-meta) 108 | m)) 109 | (transient {}) 110 | (.asMap caffeine-cache))))) 111 | 112 | (defmethod b/new-cache :memento.core/caffeine [conf] 113 | (->CaffeineCache conf (CaffeineCache_. 114 | (conf->builder conf) 115 | (:memento.core/key-fn conf) 116 | (:memento.core/ret-fn conf) 117 | (:memento.core/ret-ex-fn conf) 118 | (conf->sec-index conf)))) 119 | 120 | (defn stats 121 | "Return caffeine stats for the cache if it is a caffeine Cache. 122 | 123 | Takes a memoized fn or a Cache instance as a parameter. 124 | 125 | Returns com.github.benmanes.caffeine.cache.stats.CacheStats" 126 | [fn-or-cache] 127 | (if (instance? ICache fn-or-cache) 128 | (when (instance? CaffeineCache fn-or-cache) 129 | (.stats ^CaffeineCache_ (:caffeine-cache fn-or-cache))) 130 | (stats (.mountedCache ^IMountPoint fn-or-cache)))) 131 | 132 | (defn to-data [cache] 133 | (when-let [caffeine (:caffeine-cache cache)] 134 | (persistent! 135 | (reduce (fn [m [^CacheKey k v]] (assoc-imm-val! m 136 | [(.getId k) (.getArgs k)] 137 | v 138 | #(if (and (instance? EntryMeta %) (nil? (.getV ^EntryMeta %))) 139 | nil %))) 140 | (transient {}) 141 | (.asMap ^CaffeineCache_ caffeine))))) 142 | 143 | (defn load-data [cache data-map] 144 | (.loadData ^CaffeineCache_ (:caffeine-cache cache) data-map) 145 | cache) 146 | -------------------------------------------------------------------------------- /src/memento/mount.clj: -------------------------------------------------------------------------------- 1 | (ns memento.mount 2 | "Mount points, they serve as glue between a cache that can house entries from 3 | multiple functions and the individual functions." 4 | {:author "Rok Lenarčič"} 5 | (:require [memento.base :as base] 6 | [memento.config :as config]) 7 | (:import (clojure.lang AFn ISeq MultiFn) 8 | (memento.base ICache Segment) 9 | (memento.mount Cached CachedFn CachedMultiFn IMountPoint))) 10 | 11 | (def ^:dynamic *caches* "Contains map of mount point to cache instance" {}) 12 | (def tags "Map tag to mount-point" (atom {})) 13 | 14 | (def configuration-props [config/key-fn config/ret-fn config/seed config/tags 15 | config/evt-fn config/id config/key-fn* config/ret-ex-fn]) 16 | 17 | (defn assoc-cache-tags 18 | "Add Mount Point ref to tag index" 19 | [index cache-tags ref] 20 | (reduce #(update %1 %2 (fnil conj #{}) ref) index cache-tags)) 21 | 22 | (defn dissoc-cache-tags 23 | "Remove Mount Point ref from tag index" 24 | [index ref] 25 | (reduce-kv #(assoc %1 %2 (disj %3 ref)) {} index)) 26 | 27 | (deftype TagsUnloader [cache-mount] 28 | Runnable 29 | (run [this] 30 | (swap! tags dissoc-cache-tags cache-mount) 31 | (alter-var-root #'*caches* dissoc cache-mount) 32 | nil)) 33 | 34 | (defrecord UntaggedMountPoint [^ICache cache ^Segment segment evt-handler] 35 | IMountPoint 36 | (asMap [this] (.asMap cache segment)) 37 | (cached [this args] (.cached cache segment args)) 38 | (ifCached [this args] (.ifCached cache segment args)) 39 | (getTags [this] []) 40 | (handleEvent [this evt] (evt-handler this evt)) 41 | (invalidate [this args] (.invalidate cache segment args)) 42 | (invalidateAll [this] (.invalidate cache segment)) 43 | (mountedCache [this] cache) 44 | (addEntries [this args-to-vals] (.addEntries cache segment args-to-vals)) 45 | (segment [this] segment)) 46 | 47 | (defrecord TaggedMountPoint [tags ^Segment segment evt-handler] 48 | IMountPoint 49 | (asMap [this] (.asMap ^ICache (*caches* this base/no-cache) segment)) 50 | (cached [this args] (.cached ^ICache (*caches* this base/no-cache) segment args)) 51 | (ifCached [this args] (.ifCached ^ICache (*caches* this base/no-cache) segment args)) 52 | (getTags [this] tags) 53 | (handleEvent [this evt] (evt-handler this evt)) 54 | (invalidate [this args] (.invalidate ^ICache (*caches* this base/no-cache) segment args)) 55 | (invalidateAll [this] (.invalidate ^ICache (*caches* this base/no-cache) segment)) 56 | (mountedCache [this] (*caches* this base/no-cache)) 57 | (addEntries [this args-to-vals] 58 | (.addEntries ^ICache (*caches* this base/no-cache) segment args-to-vals)) 59 | (segment [this] segment)) 60 | 61 | (defn mounted-cache [^IMountPoint mp] (.mountedCache mp)) 62 | 63 | (defn reify-mount-conf 64 | "Transform user given mount-conf to a canonical form of a map." 65 | [mount-conf] 66 | (if (map? mount-conf) 67 | mount-conf 68 | {config/tags ((if (sequential? mount-conf) vec vector) mount-conf)})) 69 | 70 | (defn wrap-fn 71 | [f ret-fn ret-ex-fn] 72 | (cond 73 | (and ret-fn ret-ex-fn) (fn [& args] 74 | (try (ret-fn args (AFn/applyToHelper f args)) 75 | (catch Throwable t (throw (ret-ex-fn args t))))) 76 | ret-fn (fn [& args] (ret-fn args (AFn/applyToHelper f args))) 77 | ret-ex-fn (fn [& args] 78 | (try (AFn/applyToHelper f args) 79 | (catch Throwable t (throw (ret-ex-fn args t))))) 80 | :else f)) 81 | 82 | (defn create-mount 83 | "Create mount record by specified map conf" 84 | [f cache mount-conf] 85 | (let [key-fn (or (config/key-fn mount-conf) 86 | (when-let [base (config/key-fn* mount-conf)] 87 | (fn [args] (AFn/applyToHelper base (if (instance? ISeq args) args (seq args))))) 88 | identity) 89 | evt-fn (config/evt-fn mount-conf (fn [_ _] nil)) 90 | f* (wrap-fn f (config/ret-fn mount-conf) (config/ret-ex-fn mount-conf)) 91 | segment (Segment. f* key-fn (mount-conf config/id f) mount-conf)] 92 | (if-let [t (config/tags mount-conf)] 93 | (let [wrapped-t (if (sequential? t) t (vector t)) 94 | mp (->TaggedMountPoint wrapped-t segment evt-fn)] 95 | (alter-var-root #'*caches* assoc mp cache) 96 | (swap! tags assoc-cache-tags wrapped-t mp) 97 | mp) 98 | (->UntaggedMountPoint cache segment evt-fn)))) 99 | 100 | (defn bind 101 | "Bind a cache to a fn or var. Internal function." 102 | [fn-or-var mount-conf cache] 103 | (if (var? fn-or-var) 104 | (let [mount-conf (-> mount-conf 105 | reify-mount-conf 106 | (update config/id #(or % (.intern (str fn-or-var)))))] 107 | (alter-var-root fn-or-var bind mount-conf cache)) 108 | (let [mount-conf (reify-mount-conf mount-conf) 109 | constructor (if (instance? MultiFn fn-or-var) 110 | #(CachedMultiFn. (str (config/id mount-conf)) %1 %2 %3 %4) 111 | #(CachedFn. %1 %2 %3 %4)) 112 | stacking (if (instance? Cached fn-or-var) (config/bind-mode mount-conf :new) :none) 113 | ^IMountPoint cache-mount (case stacking 114 | :new (create-mount (.getOriginalFn ^Cached fn-or-var) cache mount-conf) 115 | :keep (.getMp ^Cached fn-or-var) 116 | (:none :stack) (create-mount fn-or-var cache mount-conf)) 117 | reload-guard (when (and config/reload-guards? (config/tags mount-conf) (not= :keep stacking)) 118 | (doto (Object.) 119 | (IMountPoint/register (->TagsUnloader cache-mount)))) 120 | f (case stacking 121 | :keep fn-or-var 122 | (:new :stack) (constructor reload-guard cache-mount (meta fn-or-var) (.getOriginalFn ^Cached fn-or-var)) 123 | :none (constructor reload-guard cache-mount (meta fn-or-var) fn-or-var))] 124 | (.addEntries (.getMp ^Cached f) (config/seed mount-conf {})) 125 | f))) 126 | 127 | (defn mount-point 128 | "Return active mount point from the object's meta." 129 | [obj] 130 | (when (instance? IMountPoint obj) obj)) 131 | 132 | (defn update-existing 133 | "Convenience function. Updates ks's that are present with the provided update fn." 134 | [m ks update-fn] 135 | (reduce #(if-let [kv (find %1 %2)] (assoc %1 %2 (update-fn (val kv))) %1) m ks)) 136 | 137 | (defn alter-caches-mapping 138 | "Internal function. Modifies entire tagged cache map with the provided function. 139 | Applies the function as (fn [*caches* refs & other-update-fn-args])" 140 | [tag update-fn & update-fn-args] 141 | (let [refs (get @tags tag []) 142 | update-fn #(apply update-fn % refs update-fn-args)] 143 | (if (.getThreadBinding #'*caches*) 144 | (var-set #'*caches* (update-fn *caches*)) 145 | (alter-var-root #'*caches* update-fn)))) 146 | -------------------------------------------------------------------------------- /src/memento/config.clj: -------------------------------------------------------------------------------- 1 | (ns memento.config 2 | "Memoization library config. 3 | 4 | Contains global settings that manipulate the cache mechanisms. 5 | See doc strings. 6 | 7 | Contains conf settings as vars. 8 | 9 | Also contains documented definitions of standard options of cache config." 10 | {:author "Rok Lenarčič"} 11 | (:refer-clojure :exclude [type]) 12 | (:import (java.util.concurrent TimeUnit))) 13 | 14 | (def ^:redef enabled? 15 | "If false, then all cache attach operations create a cache that does no 16 | caching (changing this value doesn't affect caches already created). 17 | 18 | Initially has the value of java property `memento.enabled` (defaulting to true)." 19 | (Boolean/valueOf (System/getProperty "memento.enabled" "true"))) 20 | 21 | (def ^:redef reload-guards? 22 | "If true, then whenever a function cached with tags is garbage collected (e.g. after a namespace reload in REPL), 23 | a cleanup is done of global tags map. Can be turned off if you don't intend to reload namespaces or do other 24 | actions that would GC cached function instances. 25 | 26 | Initially has the value of java property `memento.reloadable` (defaulting to true)." 27 | (Boolean/valueOf (System/getProperty "memento.reloadable" "true"))) 28 | 29 | (def ^:dynamic *default-type* "Default cache type." :memento.core/none) 30 | 31 | (def type 32 | "Cache setting, type of cache or region that will be instantiated, a keyword. 33 | 34 | The library has two built-ins: 35 | - memento.core/none 36 | - memento.core/caffeine 37 | 38 | If not specified the caches created default to *default-type*." 39 | :memento.core/type) 40 | 41 | (def bind-mode 42 | "Function bind setting, defaults to :new. It governs what the bind will do if you try to bind 43 | a cache to a function that is already cached, e.g. what happens when memo is called multiple times 44 | on same Var. Options are: 45 | - :keep, keeps old cache binding 46 | - :new, keeps the new cache binding 47 | - :stack, stacks the caches, so the new binding wraps the older cached function" 48 | :memento.core/bind-mode) 49 | 50 | (def key-fn 51 | "Cache and function bind setting, a function to be used to calculate the cache key (fn [f-args] key). 52 | 53 | Cache key affects what is considered the 'same' argument list for a function and it will affect caching in that manner. 54 | 55 | If both function bind and cache have this setting, then function bind key-fn is applied first. 56 | 57 | It's a function of 1 argument, the seq of function arguments. If not provided it defaults to identity." 58 | :memento.core/key-fn) 59 | 60 | (def key-fn* 61 | "Function bind setting, works same as key-fn but the provided function will receive all 62 | arguments that the original function does. If both key-fn and key-fn* are provided, key-fn is used." 63 | :memento.core/key-fn*) 64 | 65 | (def ret-fn 66 | "Cache and function bind setting, a function that is ran to process the return of the function, before it's memoized, 67 | (fn [fn-args ret-value] transformed-value). 68 | 69 | It can provide some generic transformation facility, but more importantly, it can wrap specific return 70 | values in 'do-not-cache' object, that prevents caching or wrap with tagged IDs." 71 | :memento.core/ret-fn) 72 | 73 | (def evt-fn 74 | "Function bind setting, a function that is invoked when any event is fired at the function. 75 | 76 | (fn [mnt-point event] void) 77 | 78 | Useful generally to push data into the related cache, the mnt-point parameter implement MountPoint protocol 79 | so you can invoke memo-add! and such on it. The event can be any data, it's probably best to come up with 80 | a format that enables the functions that receive the event to be able to tell them apart." 81 | :memento.core/evt-fn) 82 | 83 | (def seed 84 | "Function bind setting, a map of cache keys to values that will be preloaded when cache is bound." 85 | :memento.core/seed) 86 | 87 | (def ^:deprecated guava 88 | "DEPRECATED: Cache setting value, now points to caffeine implementation" 89 | :memento.core/caffeine) 90 | 91 | (def caffeine 92 | "Cache setting value, type name of Caffeine cache implementation" 93 | :memento.core/caffeine) 94 | 95 | (def none 96 | "Cache setting value, type name of noop cache implementation" 97 | :memento.core/none) 98 | 99 | (def ^:deprecated concurrency 100 | "DEPRECATED: it does nothing in Caffeine implementation" 101 | :memento.core/concurrency) 102 | 103 | (def initial-capacity 104 | "Cache setting, supported by: caffeine, an int. 105 | 106 | Sets the minimum total size for the internal hash tables. Providing a large enough estimate 107 | at construction time avoids the need for expensive resizing operations later, 108 | but setting this value unnecessarily high wastes memory." 109 | :memento.core/initial-capacity) 110 | 111 | (def size< 112 | "Cache setting, supported by: caffeine, a long. 113 | 114 | Specifies the maximum number of entries the cache may contain. Some implementations might evict entries 115 | even before the number of entries reaches the limit." 116 | :memento.core/size<) 117 | 118 | (def ttl 119 | "Cache and Function bind setting, a duration. 120 | 121 | Specifies that each entry should be automatically removed from the cache once a duration 122 | has elapsed after the entry's creation, or the most recent replacement of its value via a put. 123 | 124 | Duration is specified by a number (of seconds) or a vector of a number and unit keyword. 125 | So ttl of 10 or [10 :s] is the same. See 'timeunits' var." 126 | :memento.core/ttl) 127 | 128 | (def fade 129 | "Cache and Function bind setting, a duration. 130 | 131 | Specifies that each entry should be automatically removed from the cache once a fixed duration 132 | has elapsed after the entry's creation, the most recent replacement of its value, or its last access. 133 | Access time is reset by all cache read and write operations. 134 | 135 | Duration is specified by a number (of seconds) or a vector of a number and unit keyword. 136 | So fade of 10 or [10 :s] is the same. See 'timeunits' var." 137 | :memento.core/fade) 138 | 139 | (def tags 140 | "Function bind setting. 141 | 142 | List of tags for this memoized bind." 143 | :memento.core/tags) 144 | 145 | (def id 146 | "Function bind setting. 147 | 148 | Id of the function bind. If you're memoizing a Var, this defaults to stringified var name, 149 | otherwise the ID is the function itself. 150 | 151 | This is useful to specify when you're using Cache implementation that stores data outside JVM, 152 | as they often need a name for each function's cache." 153 | :memento.core/id) 154 | 155 | (def timeunits 156 | "Timeunits keywords, corresponds with Durations class." 157 | {:ns TimeUnit/NANOSECONDS 158 | :us TimeUnit/MICROSECONDS 159 | :ms TimeUnit/MILLISECONDS 160 | :s TimeUnit/SECONDS 161 | :m TimeUnit/MINUTES 162 | :h TimeUnit/HOURS 163 | :d TimeUnit/DAYS}) 164 | 165 | (def cache 166 | "The key extracted from object/var meta and used as cache configuration when 167 | 1-arg memo is called or ns-scan based mounting is performed." 168 | :memento.core/cache) 169 | 170 | (def mount 171 | "The key extracted from object/var meta and used as mount configuration when 172 | 1-arg memo is called or ns-scan based mounting is performed." 173 | :memento.core/mount) 174 | 175 | (def ret-ex-fn 176 | "Cache and function bind setting, a function that is ran to process the throwable thrown by the function, 177 | (fn [fn-args throwable] throwable)." 178 | :memento.core/ret-ex-fn) 179 | -------------------------------------------------------------------------------- /doc/performance.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | Performance is not a dedicated goal of this library, but here's some numbers: 4 | 5 |  6 | 7 |  8 | 9 | ```clojure 10 | ; memoize is not thread-safe and doesn't have any features 11 | (def f-memoize (memoize identity)) 12 | ; clojure.core.memoize 13 | (def f-core-memo (ccm/memo identity)) 14 | ; memento 15 | (def f-memento (m/memo identity {::m/type ::m/caffeine})) 16 | ; memento caffeine variable expiry 17 | (def f-memento-var (m/memo identity {::m/type ::m/caffeine ::m/expiry memento.caffeine.config/meta-expiry})) 18 | ; memento light caffeine 19 | (def f-light-memento (m/memo identity {::m/type ::m/light-caffeine})) 20 | ``` 21 | ## Memoize 22 | 23 | #### All hits 24 | ```text 25 | (cc/bench (f-memoize 1)) 26 | Evaluation count : 2911575540 in 60 samples of 48526259 calls. 27 | Execution time mean : 18,520670 ns 28 | Execution time std-deviation : 0,632964 ns 29 | Execution time lower quantile : 18,041806 ns ( 2,5%) 30 | Execution time upper quantile : 20,272312 ns (97,5%) 31 | Overhead used : 1,997090 ns 32 | 33 | Found 2 outliers in 60 samples (3,3333 %) 34 | low-severe 2 (3,3333 %) 35 | Variance from outliers : 20,6200 % Variance is moderately inflated by outliers 36 | 37 | ``` 38 | 39 | #### 1M misses (426ns per miss) 40 | ```text 41 | (cc/bench (let [f-memoize (memoize identity)] 42 | (reduce #(f-memoize %2) (range 1000000)))) 43 | Evaluation count : 180 in 60 samples of 3 calls. 44 | Execution time mean : 426,691729 ms 45 | Execution time std-deviation : 31,649211 ms 46 | Execution time lower quantile : 407,433346 ms ( 2,5%) 47 | Execution time upper quantile : 500,285216 ms (97,5%) 48 | Overhead used : 1,997090 ns 49 | 50 | Found 9 outliers in 60 samples (15,0000 %) 51 | low-severe 5 (8,3333 %) 52 | low-mild 4 (6,6667 %) 53 | Variance from outliers : 55,1467 % Variance is severely inflated by outliers 54 | ``` 55 | 56 | ## Clojure Core Memoize 57 | 58 | #### All hits 59 | 60 | ```text 61 | (cc/bench (f-core-memo 1)) 62 | Evaluation count : 329229720 in 60 samples of 5487162 calls. 63 | Execution time mean : 180,803852 ns 64 | Execution time std-deviation : 3,880666 ns 65 | Execution time lower quantile : 177,830691 ns ( 2,5%) 66 | Execution time upper quantile : 189,061520 ns (97,5%) 67 | Overhead used : 1,997090 ns 68 | 69 | Found 6 outliers in 60 samples (10,0000 %) 70 | low-severe 3 (5,0000 %) 71 | low-mild 3 (5,0000 %) 72 | Variance from outliers : 9,4347 % Variance is slightly inflated by outliers 73 | ``` 74 | 75 | #### 1M misses (778 ns per miss) 76 | 77 | ```text 78 | (cc/bench (let [f-core-memo (ccm/memo identity)] 79 | (reduce #(f-core-memo %2) (range 1000000)))) 80 | Evaluation count : 120 in 60 samples of 2 calls. 81 | Execution time mean : 778,758053 ms 82 | Execution time std-deviation : 58,068726 ms 83 | Execution time lower quantile : 717,950541 ms ( 2,5%) 84 | Execution time upper quantile : 947,641405 ms (97,5%) 85 | Overhead used : 1,997090 ns 86 | 87 | Found 6 outliers in 60 samples (10,0000 %) 88 | low-severe 4 (6,6667 %) 89 | low-mild 2 (3,3333 %) 90 | Variance from outliers : 55,1627 % Variance is severely inflated by outliers 91 | ``` 92 | 93 | #### 1M misses for size 100 LRU cache (1811 ns per miss) 94 | 95 | ```text 96 | (cc/bench (let [f-core-memo (ccm/lru identity :lru/threshold 100)] 97 | (reduce #(f-core-memo %2) (range 1000000)))) 98 | Evaluation count : 60 in 60 samples of 1 calls. 99 | Execution time mean : 1,811235 sec 100 | Execution time std-deviation : 23,960121 ms 101 | Execution time lower quantile : 1,773504 sec ( 2,5%) 102 | Execution time upper quantile : 1,866470 sec (97,5%) 103 | Overhead used : 1,997090 ns 104 | 105 | Found 2 outliers in 60 samples (3,3333 %) 106 | low-severe 2 (3,3333 %) 107 | Variance from outliers : 1,6389 % Variance is slightly inflated by outliers 108 | 109 | ``` 110 | 111 | ## Memento 112 | 113 | #### All hits 114 | 115 | ```text 116 | (cc/bench (f-memento 1)) 117 | 118 | Evaluation count : 854138220 in 60 samples of 14235637 calls. 119 | Execution time mean : 70,745055 ns 120 | Execution time std-deviation : 2,570125 ns 121 | Execution time lower quantile : 68,659819 ns ( 2,5%) 122 | Execution time upper quantile : 74,128774 ns (97,5%) 123 | Overhead used : 1,970580 ns 124 | 125 | Found 2 outliers in 60 samples (3,3333 %) 126 | low-severe 1 (1,6667 %) 127 | low-mild 1 (1,6667 %) 128 | Variance from outliers : 22,2591 % Variance is moderately inflated by outliers 129 | 130 | 131 | ``` 132 | 133 | #### 1M misses (474 ns per miss) 134 | 135 | ```text 136 | (cc/bench (let [f-memento (m/memo identity {::m/type ::m/caffeine})] 137 | (reduce #(f-memento %2) (range 1000000)))) 138 | Evaluation count : 120 in 60 samples of 2 calls. 139 | Execution time mean : 474,650866 ms 140 | Execution time std-deviation : 76,082064 ms 141 | Execution time lower quantile : 365,465019 ms ( 2,5%) 142 | Execution time upper quantile : 641,739223 ms (97,5%) 143 | Overhead used : 1,992837 ns 144 | 145 | ``` 146 | 147 | #### 1M misses for size 100 LRU cache (338 ns per miss) 148 | 149 | ```text 150 | (cc/bench (let [f-memento (m/memo identity {::m/size< 100 ::m/type ::m/caffeine})] 151 | (reduce #(f-memento %2) (range 1000000)))) 152 | Evaluation count : 180 in 60 samples of 3 calls. 153 | Execution time mean : 338,339882 ms 154 | Execution time std-deviation : 15,865012 ms 155 | Execution time lower quantile : 321,764748 ms ( 2,5%) 156 | Execution time upper quantile : 370,249429 ms (97,5%) 157 | Overhead used : 1,970580 ns 158 | 159 | Found 4 outliers in 60 samples (6,6667 %) 160 | low-severe 3 (5,0000 %) 161 | low-mild 1 (1,6667 %) 162 | Variance from outliers : 33,5491 % Variance is moderately inflated by outliers 163 | 164 | 165 | ``` 166 | 167 | ## Memento Variable Expiry 168 | 169 | #### All hits 170 | 171 | ```text 172 | (cc/bench (f-memento-var 1)) 173 | 174 | Evaluation count : 453412980 in 60 samples of 7556883 calls. 175 | Execution time mean : 132,501700 ns 176 | Execution time std-deviation : 2,015071 ns 177 | Execution time lower quantile : 130,326931 ns ( 2,5%) 178 | Execution time upper quantile : 134,890796 ns (97,5%) 179 | Overhead used : 1,978672 ns 180 | 181 | ``` 182 | 183 | #### 1M misses (526 ns per miss) 184 | 185 | ```text 186 | (cc/bench (let [f-memento-var (m/memo identity {::m/type ::m/caffeine ::m/expiry memento.caffeine.config/meta-expiry})] 187 | (reduce #(f-memento-var %2) (range 1000000)))) 188 | Evaluation count : 120 in 60 samples of 2 calls. 189 | Execution time mean : 526,197766 ms 190 | Execution time std-deviation : 59,110910 ms 191 | Execution time lower quantile : 426,811124 ms ( 2,5%) 192 | Execution time upper quantile : 644,451645 ms (97,5%) 193 | Overhead used : 1,978672 ns 194 | 195 | ``` 196 | 197 | #### 1M misses for size 100 LRU cache (387 ns per miss) 198 | 199 | ```text 200 | (cc/bench (let [f-memento-var (m/memo identity {::m/size< 100 ::m/type ::m/caffeine ::m/expiry memento.caffeine.config/meta-expiry})] 201 | (reduce #(f-memento-var %2) (range 1000000)))) 202 | Evaluation count : 180 in 60 samples of 3 calls. 203 | Execution time mean : 423,554590 ms 204 | Execution time std-deviation : 7,825220 ms 205 | Execution time lower quantile : 414,372683 ms ( 2,5%) 206 | Execution time upper quantile : 435,451863 ms (97,5%) 207 | Overhead used : 1,978672 ns 208 | 209 | ``` -------------------------------------------------------------------------------- /java/memento/caffeine/CaffeineCache_.java: -------------------------------------------------------------------------------- 1 | package memento.caffeine; 2 | 3 | import clojure.lang.*; 4 | import com.github.benmanes.caffeine.cache.Cache; 5 | import com.github.benmanes.caffeine.cache.Caffeine; 6 | import com.github.benmanes.caffeine.cache.RemovalListener; 7 | import com.github.benmanes.caffeine.cache.stats.CacheStats; 8 | import memento.base.CacheKey; 9 | import memento.base.EntryMeta; 10 | import memento.base.LockoutMap; 11 | import memento.base.Segment; 12 | 13 | import java.util.*; 14 | import java.util.concurrent.ConcurrentHashMap; 15 | import java.util.concurrent.ConcurrentMap; 16 | import java.util.function.BiFunction; 17 | 18 | public class CaffeineCache_ { 19 | 20 | private final BiFunction keyFn; 21 | 22 | private final SecondaryIndex secIndex; 23 | private final IFn retFn; 24 | 25 | private final IFn retExFn; 26 | 27 | private final Cache delegate; 28 | 29 | private final Set loads = ConcurrentHashMap.newKeySet(); 30 | 31 | public CaffeineCache_(Caffeine builder, final IFn keyFn, final IFn retFn, final IFn retExFn, SecondaryIndex secIndex) { 32 | this.keyFn = keyFn == null ? 33 | (segment, args) -> new CacheKey(segment.getId(), segment.getKeyFn().invoke(args)) : 34 | (segment, args) -> new CacheKey(segment.getId(), keyFn.invoke(segment.getKeyFn().invoke(args))); 35 | this.retFn = retFn; 36 | this.delegate = builder.build(); 37 | this.secIndex = secIndex; 38 | this.retExFn = retExFn; 39 | } 40 | 41 | private void initLoad(SpecialPromise promise) { 42 | promise.init(); 43 | loads.add(promise); 44 | } 45 | 46 | public Object cached(Segment segment, ISeq args) throws Throwable { 47 | CacheKey key = keyFn.apply(segment, args); 48 | do { 49 | SpecialPromise p = new SpecialPromise(); 50 | // check for ongoing load 51 | Object cached = delegate.asMap().putIfAbsent(key, p); 52 | if (cached == null) { 53 | try { 54 | initLoad(p); 55 | // calculate value 56 | Object result = AFn.applyToHelper(segment.getF(), args); 57 | if (retFn != null) { 58 | result = retFn.invoke(args, result); 59 | } 60 | if (!p.deliver(result)) { 61 | // The SpecialPromise was invalidated, restart the process 62 | delegate.asMap().remove(key, p); 63 | continue; 64 | } 65 | // if valid add to secondary index 66 | secIndex.add(key, result); 67 | if (result instanceof EntryMeta && ((EntryMeta) result).isNoCache()) { 68 | delegate.asMap().remove(key, p); 69 | } else { 70 | delegate.asMap().replace(key, p, result == null ? EntryMeta.NIL : result); 71 | } 72 | return EntryMeta.unwrap(result); 73 | } catch (Throwable t) { 74 | delegate.asMap().remove(key, p); 75 | if (!p.isInvalid()) { 76 | p.deliverException(retExFn == null ? t : (Throwable) retExFn.invoke(args, t)); 77 | throw t; 78 | } 79 | } finally { 80 | p.releaseResult(); 81 | loads.remove(p); 82 | } 83 | } else { 84 | // join into ongoing load 85 | if (cached instanceof SpecialPromise) { 86 | SpecialPromise sp = (SpecialPromise) cached; 87 | Object ret = sp.await(key); 88 | if (ret != EntryMeta.absent && !LockoutMap.awaitLockout(ret)) { 89 | // if not invalidated, return the value 90 | return EntryMeta.unwrap(ret); 91 | } 92 | } else { 93 | if (!LockoutMap.awaitLockout(cached)) { 94 | // if not invalidated, return the value 95 | return EntryMeta.unwrap(cached); 96 | } 97 | } 98 | // else try to initiate load again 99 | } 100 | } while (true); 101 | } 102 | 103 | public Object ifCached(Segment segment, ISeq args) throws Throwable { 104 | CacheKey key = keyFn.apply(segment, args); 105 | Object v = delegate.getIfPresent(key); 106 | Object absent = EntryMeta.absent; 107 | if (v == null) { 108 | return absent; 109 | } else if (v instanceof SpecialPromise) { 110 | SpecialPromise p = (SpecialPromise) v; 111 | Object ret = p.getNow(); 112 | if (ret == absent || LockoutMap.awaitLockout(ret)) { 113 | return absent; 114 | } else { 115 | return EntryMeta.unwrap(ret); 116 | } 117 | } else { 118 | return LockoutMap.awaitLockout(v) ? absent : EntryMeta.unwrap(v); 119 | } 120 | } 121 | 122 | public void invalidate(Segment segment) { 123 | final Iterator> iter = delegate.asMap().entrySet().iterator(); 124 | while (iter.hasNext()) { 125 | Map.Entry it = iter.next(); 126 | if (it.getKey().getId().equals(segment.getId())) { 127 | Object v = it.getValue(); 128 | if (v instanceof SpecialPromise) { 129 | ((SpecialPromise) v).invalidate(); 130 | } 131 | iter.remove(); 132 | } 133 | } 134 | } 135 | 136 | public void invalidate(Segment segment, ISeq args) { 137 | Object v = delegate.asMap().remove(keyFn.apply(segment, args)); 138 | if (v instanceof SpecialPromise) { 139 | ((SpecialPromise) v).invalidate(); 140 | } 141 | } 142 | 143 | public void invalidateAll() { 144 | delegate.invalidateAll(); 145 | } 146 | 147 | public void invalidateIds(Iterable ids) { 148 | HashSet keys = new HashSet<>(); 149 | for (Object id : ids) { 150 | secIndex.drainKeys(id, keys::add); 151 | } 152 | ConcurrentMap map = delegate.asMap(); 153 | for (CacheKey k : keys) { 154 | Object removed = map.remove(k); 155 | if (removed instanceof SpecialPromise) { 156 | ((SpecialPromise) removed).invalidate(); 157 | } 158 | } 159 | loads.forEach(row -> row.addInvalidIds(ids)); 160 | } 161 | 162 | public void addEntries(Segment segment, IPersistentMap argsToVals) { 163 | for (Object o : argsToVals) { 164 | MapEntry entry = (MapEntry) o; 165 | CacheKey key = keyFn.apply(segment, RT.seq(entry.getKey())); 166 | Object val = entry.getValue(); 167 | secIndex.add(key, val); 168 | delegate.put(key, val == null ? EntryMeta.NIL : val); 169 | } 170 | } 171 | 172 | public ConcurrentMap asMap() { 173 | return delegate.asMap(); 174 | } 175 | 176 | public CacheStats stats() { 177 | return delegate.stats(); 178 | } 179 | 180 | public void loadData(Map map) { 181 | map.forEach((Object k, Object v) -> { 182 | List list = (List) k; 183 | CacheKey key = new CacheKey(list.get(0), list.get(1)); 184 | secIndex.add(key, v); 185 | delegate.put(key, v == null ? EntryMeta.NIL : v); 186 | }); 187 | } 188 | 189 | public static RemovalListener listener(IFn removalListener) { 190 | return (k, v, removalCause) -> { 191 | if (!(v instanceof SpecialPromise)) { 192 | removalListener.invoke(k.getId(), k.getArgs(), v instanceof EntryMeta ? ((EntryMeta) v).getV() : v, removalCause); 193 | } 194 | }; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /java/memento/multi/TieredCache.java: -------------------------------------------------------------------------------- 1 | package memento.multi; 2 | 3 | import clojure.lang.ArraySeq; 4 | import clojure.lang.IFn; 5 | import clojure.lang.IPersistentMap; 6 | import clojure.lang.ISeq; 7 | import memento.base.ICache; 8 | import memento.base.Segment; 9 | 10 | public class TieredCache extends MultiCache { 11 | public TieredCache(ICache cache, ICache upstream, IPersistentMap conf, Object absent) { 12 | super(cache, upstream, conf, absent); 13 | } 14 | 15 | @Override 16 | public Object cached(Segment segment, ISeq args) { 17 | return cache.cached(segment.withFn(new AskUpstream(segment)), args); 18 | } 19 | 20 | private class AskUpstream implements IFn { 21 | 22 | private final Segment segment; 23 | 24 | public AskUpstream(Segment segment) { 25 | this.segment = segment; 26 | } 27 | 28 | @Override 29 | public Object call() { 30 | return upstream.cached(segment, ArraySeq.create()); 31 | } 32 | 33 | @Override 34 | public void run() { 35 | upstream.cached(segment, ArraySeq.create()); 36 | } 37 | 38 | @Override 39 | public Object invoke() { 40 | return upstream.cached(segment, ArraySeq.create()); 41 | } 42 | 43 | @Override 44 | public Object invoke(Object arg1) { 45 | return upstream.cached(segment, ArraySeq.create(arg1)); 46 | } 47 | 48 | @Override 49 | public Object invoke(Object arg1, Object arg2) { 50 | return upstream.cached(segment, ArraySeq.create(arg1, arg2)); 51 | } 52 | 53 | @Override 54 | public Object invoke(Object arg1, Object arg2, Object arg3) { 55 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3)); 56 | } 57 | 58 | @Override 59 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 60 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4)); 61 | } 62 | 63 | @Override 64 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 65 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5)); 66 | } 67 | 68 | @Override 69 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 70 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6)); 71 | } 72 | 73 | @Override 74 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 75 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7)); 76 | } 77 | 78 | @Override 79 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 80 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)); 81 | } 82 | 83 | @Override 84 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 85 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)); 86 | } 87 | 88 | @Override 89 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 90 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); 91 | } 92 | 93 | @Override 94 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 95 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11)); 96 | } 97 | 98 | @Override 99 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 100 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12)); 101 | } 102 | 103 | @Override 104 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 105 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13)); 106 | } 107 | 108 | @Override 109 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 110 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14)); 111 | } 112 | 113 | @Override 114 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 115 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15)); 116 | } 117 | 118 | @Override 119 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 120 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16)); 121 | } 122 | 123 | @Override 124 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 125 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17)); 126 | } 127 | 128 | @Override 129 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 130 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18)); 131 | } 132 | 133 | @Override 134 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 135 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19)); 136 | } 137 | 138 | @Override 139 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 140 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20)); 141 | } 142 | 143 | @Override 144 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 145 | Object[] allArgs = new Object[20 + args.length]; 146 | System.arraycopy(args, 0, allArgs, 20, args.length); 147 | allArgs[0] = arg1; 148 | allArgs[1] = arg2; 149 | allArgs[2] = arg3; 150 | allArgs[3] = arg4; 151 | allArgs[4] = arg5; 152 | allArgs[5] = arg6; 153 | allArgs[6] = arg7; 154 | allArgs[7] = arg8; 155 | allArgs[8] = arg9; 156 | allArgs[9] = arg10; 157 | allArgs[10] = arg11; 158 | allArgs[11] = arg12; 159 | allArgs[12] = arg13; 160 | allArgs[13] = arg14; 161 | allArgs[14] = arg15; 162 | allArgs[15] = arg16; 163 | allArgs[16] = arg17; 164 | allArgs[17] = arg18; 165 | allArgs[18] = arg19; 166 | allArgs[19] = arg20; 167 | return upstream.cached(segment, ArraySeq.create(allArgs)); 168 | } 169 | 170 | @Override 171 | public Object applyTo(ISeq arglist) { 172 | return upstream.cached(segment, arglist); 173 | } 174 | 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /java/memento/mount/CachedFn.java: -------------------------------------------------------------------------------- 1 | package memento.mount; 2 | 3 | import clojure.lang.*; 4 | import memento.base.ICache; 5 | import memento.base.Segment; 6 | 7 | public class CachedFn extends AFunction implements IMountPoint, Cached { 8 | private final Object reloadGuard; 9 | private final IFn originalFn; 10 | private final Segment segment; 11 | 12 | @Override 13 | public IPersistentMap asMap() { 14 | return mp.asMap(); 15 | } 16 | 17 | @Override 18 | public Object cached(ISeq args) { 19 | return mp.cached(args); 20 | } 21 | 22 | @Override 23 | public Object ifCached(ISeq args) { 24 | return mp.ifCached(args); 25 | } 26 | 27 | @Override 28 | public Object getTags() { 29 | return mp.getTags(); 30 | } 31 | 32 | @Override 33 | public Object handleEvent(Object event) { 34 | return mp.handleEvent(event); 35 | } 36 | 37 | @Override 38 | public ICache invalidate(ISeq args) { 39 | return mp.invalidate(args); 40 | } 41 | 42 | @Override 43 | public ICache invalidateAll() { 44 | return mp.invalidateAll(); 45 | } 46 | 47 | @Override 48 | public ICache mountedCache() { 49 | return mp.mountedCache(); 50 | } 51 | 52 | @Override 53 | public Segment segment() { 54 | return mp.segment(); 55 | } 56 | 57 | @Override 58 | public ICache addEntries(IPersistentMap argsToVals) { 59 | return mp.addEntries(argsToVals); 60 | } 61 | 62 | private final IMountPoint mp; 63 | private final IPersistentMap meta; 64 | 65 | public CachedFn(Object reloadGuard, IMountPoint mp, IPersistentMap meta, IFn originalFn) { 66 | this.reloadGuard = reloadGuard; 67 | this.mp = mp; 68 | this.meta = meta; 69 | this.originalFn = originalFn; 70 | this.segment = mp.segment(); 71 | } 72 | 73 | @Override 74 | public IPersistentMap meta() { 75 | return meta; 76 | } 77 | 78 | @Override 79 | public IObj withMeta(IPersistentMap meta) { 80 | return new CachedFn(reloadGuard, mp, meta, originalFn); 81 | } 82 | 83 | @Override 84 | public Object call() { 85 | return mp.mountedCache().cached(segment, ArraySeq.create()); 86 | } 87 | 88 | @Override 89 | public void run() { 90 | mp.mountedCache().cached(segment, ArraySeq.create()); 91 | } 92 | 93 | @Override 94 | public Object invoke() { 95 | return mp.mountedCache().cached(segment, ArraySeq.create()); 96 | } 97 | 98 | @Override 99 | public Object invoke(Object arg1) { 100 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1)); 101 | } 102 | 103 | @Override 104 | public Object invoke(Object arg1, Object arg2) { 105 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2)); 106 | } 107 | 108 | @Override 109 | public Object invoke(Object arg1, Object arg2, Object arg3) { 110 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3)); 111 | } 112 | 113 | @Override 114 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 115 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4)); 116 | } 117 | 118 | @Override 119 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 120 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5)); 121 | } 122 | 123 | @Override 124 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 125 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6)); 126 | } 127 | 128 | @Override 129 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 130 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7)); 131 | } 132 | 133 | @Override 134 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 135 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)); 136 | } 137 | 138 | @Override 139 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 140 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)); 141 | } 142 | 143 | @Override 144 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 145 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); 146 | } 147 | 148 | @Override 149 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 150 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11)); 151 | } 152 | 153 | @Override 154 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 155 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12)); 156 | } 157 | 158 | @Override 159 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 160 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13)); 161 | } 162 | 163 | @Override 164 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 165 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14)); 166 | } 167 | 168 | @Override 169 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 170 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15)); 171 | } 172 | 173 | @Override 174 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 175 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16)); 176 | } 177 | 178 | @Override 179 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 180 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17)); 181 | } 182 | 183 | @Override 184 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 185 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18)); 186 | } 187 | 188 | @Override 189 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 190 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19)); 191 | } 192 | 193 | @Override 194 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 195 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20)); 196 | } 197 | 198 | @Override 199 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 200 | Object[] allArgs = new Object[20 + args.length]; 201 | System.arraycopy(args, 0, allArgs, 20, args.length); 202 | allArgs[0] = arg1; 203 | allArgs[1] = arg2; 204 | allArgs[2] = arg3; 205 | allArgs[3] = arg4; 206 | allArgs[4] = arg5; 207 | allArgs[5] = arg6; 208 | allArgs[6] = arg7; 209 | allArgs[7] = arg8; 210 | allArgs[8] = arg9; 211 | allArgs[9] = arg10; 212 | allArgs[10] = arg11; 213 | allArgs[11] = arg12; 214 | allArgs[12] = arg13; 215 | allArgs[13] = arg14; 216 | allArgs[14] = arg15; 217 | allArgs[15] = arg16; 218 | allArgs[16] = arg17; 219 | allArgs[17] = arg18; 220 | allArgs[18] = arg19; 221 | allArgs[19] = arg20; 222 | return mp.mountedCache().cached(segment, ArraySeq.create(allArgs)); 223 | } 224 | 225 | @Override 226 | public Object applyTo(ISeq arglist) { 227 | return mp.mountedCache().cached(segment, arglist); 228 | } 229 | 230 | public IMountPoint getMp() { 231 | return mp; 232 | } 233 | 234 | public IFn getOriginalFn() { 235 | return originalFn; 236 | } 237 | 238 | @Override 239 | public String toString() { 240 | return "CachedFn{" + 241 | "originalFn=" + originalFn + 242 | ", segment=" + segment + 243 | ", mp=" + mp + 244 | ", meta=" + meta + 245 | '}'; 246 | } 247 | 248 | public Segment getSegment() { 249 | return segment; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/memento/core.clj: -------------------------------------------------------------------------------- 1 | (ns memento.core 2 | "Memoization library." 3 | {:author "Rok Lenarčič"} 4 | (:require [memento.base :as base] 5 | [memento.caffeine] 6 | [memento.multi :as multi] 7 | [memento.mount :as mount]) 8 | (:import (java.util IdentityHashMap) 9 | (java.util.function BiFunction) 10 | (memento.base EntryMeta ICache LockoutTag) 11 | (memento.mount Cached IMountPoint))) 12 | 13 | (defn do-not-cache 14 | "Wrap a function result value in a wrapper that tells the Cache not to 15 | cache this particular value." 16 | [v] 17 | (if (instance? EntryMeta v) 18 | (do (.setNoCache ^EntryMeta v true) v) 19 | (EntryMeta. v true #{}))) 20 | 21 | (defn with-tag-id 22 | "Wrap a function result value in a wrapper that has the given additional 23 | tag + ID information. You can add multiple IDs for same tag. 24 | 25 | This information is later used by memo-clear-tag!." 26 | [v tag id] 27 | (if (instance? EntryMeta v) 28 | (do (.setTagIdents ^EntryMeta v (conj (.getTagIdents ^EntryMeta v) [tag id])) v) 29 | (EntryMeta. v false #{[tag id]}))) 30 | 31 | (defn create 32 | "Create a cache. 33 | 34 | A conf is a map of cache settings, see memento.config namespace for names of settings." 35 | [conf] 36 | (base/base-create-cache conf)) 37 | 38 | (defn bind 39 | "Bind the cache to a function or a var. If a var is specified, then var root 40 | binding is modified. 41 | 42 | The mount-conf is a configuration options for mount point. 43 | 44 | It can be a map with options, a vector of tags, or one tag. 45 | 46 | Supported options are: 47 | - memento.core/key-fn 48 | - memento.core/key-fn* 49 | - memento.core/ret-fn 50 | - memento.core/tags 51 | - memento.core/seed" 52 | [fn-or-var mount-conf cache] 53 | (when-not (instance? ICache cache) 54 | (throw (IllegalArgumentException. "Argument should satisfy memento.base/Cache"))) 55 | (mount/bind fn-or-var mount-conf cache)) 56 | 57 | (defn memo 58 | "Combines cache create and bind operations from this namespace. 59 | 60 | If conf is provided, it is used as mount-conf in bind operation, but with any extra map keys 61 | going into cache create configuration. 62 | 63 | If no configuration is provided, meta of the fn or var is examined. 64 | 65 | The value of :memento.core/cache meta key is used as conf parameter 66 | in memento.core/memo. If :memento.core/mount key is also present, then 67 | they are used as cache and conf parameters respectively." 68 | ([fn-or-var] 69 | (let [{::keys [mount cache]} (meta fn-or-var)] 70 | (if mount (memo fn-or-var mount cache) 71 | (memo fn-or-var cache)))) 72 | ([fn-or-var conf] 73 | (if (map? conf) 74 | (memo fn-or-var 75 | (select-keys conf mount/configuration-props) 76 | (apply dissoc conf mount/configuration-props)) 77 | (memo fn-or-var conf {}))) 78 | ([fn-or-var mount-conf cache-conf] 79 | (->> cache-conf 80 | create 81 | (bind fn-or-var mount-conf)))) 82 | 83 | (defmacro defmemo 84 | "Like defn, but immediately wraps var in a memo call. It expects caching configuration 85 | to be in meta under memento.core/cache key, as expected by memo." 86 | {:arglists '([name doc-string? attr-map? [params*] prepost-map? body] 87 | [name doc-string? attr-map? ([params*] prepost-map? body)+ attr-map?])} 88 | [& body] 89 | `(memo (defn ~@body))) 90 | 91 | (defn active-cache 92 | "Return Cache instance from the function, if present." 93 | [f] (some-> (mount/mount-point f) mount/mounted-cache)) 94 | 95 | (defn memoized? 96 | "Returns true if function is memoized." 97 | [f] (instance? Cached f)) 98 | 99 | (defn memo-unwrap 100 | "Takes a function and returns an uncached function." 101 | [f] (if (instance? Cached f) (.getOriginalFn ^Cached f) f)) 102 | 103 | (defn memo-clear-cache! 104 | "Invalidate all entries in Cache. Returns cache." 105 | [cache] 106 | (when-not (instance? ICache cache) 107 | (throw (IllegalArgumentException. "Argument should satisfy memento.base/Cache"))) 108 | (base/invalidate-all cache)) 109 | 110 | (defn none-cache? 111 | "Returns true if this cache is one that does no caching." 112 | [cache] 113 | (= cache base/no-cache)) 114 | 115 | (defn memo-clear! 116 | "Invalidate one entry (f with arglist) on memoized function f, 117 | or invalidate all entries for memoized function. Returns f." 118 | ([f] 119 | (when-let [^IMountPoint mp (mount/mount-point f)] (.invalidateAll mp)) 120 | f) 121 | ([f & fargs] 122 | (when-let [^IMountPoint mp (mount/mount-point f)] (.invalidate mp fargs)) 123 | f)) 124 | 125 | (defn memo-add! 126 | "Add map's entries to the cache. The keys are argument-lists. 127 | 128 | Returns f." 129 | [f m] 130 | (when-let [^IMountPoint mp (mount/mount-point f)] (.addEntries mp m)) 131 | f) 132 | 133 | (defn as-map 134 | "Return a map representation of the memoized entries on this function." 135 | [f] 136 | (when-let [^IMountPoint mp (mount/mount-point f)] (.asMap mp))) 137 | 138 | (defn tags 139 | "Return tags of the memoized function." 140 | [f] 141 | (when-let [^IMountPoint mp (mount/mount-point f)] (.getTags mp))) 142 | 143 | (defn mounts-by-tag 144 | "Returns a sequence of MountPoint instances used by memoized functions which are tagged by this tag." 145 | [tag] 146 | (get @mount/tags tag [])) 147 | 148 | (defn caches-by-tag 149 | "Returns a collection of distinct caches that are mounted with a tag" 150 | [tag] 151 | (let [m (IdentityHashMap.)] 152 | (run! #(.put m (mount/mounted-cache %) nil) (mounts-by-tag tag)) 153 | (.keySet m))) 154 | 155 | (defn fire-event! 156 | "Fire an event payload to the single cached function or all tagged functions, if tag 157 | is provided." 158 | [f-or-tag evt] 159 | (if (instance? IMountPoint f-or-tag) 160 | (.handleEvent ^IMountPoint f-or-tag evt) 161 | (->> (mounts-by-tag f-or-tag) 162 | (eduction (map #(.handleEvent ^IMountPoint % evt))) 163 | dorun))) 164 | 165 | (defn memo-clear-tags! 166 | "Invalidate all entries that have the specified tag + id metadata. ID can be anything. 167 | 168 | Expects a collection of [tag id] pairs." 169 | [& tag+ids] 170 | (let [cache->ids (IdentityHashMap.) 171 | _ (doseq [[tag tag+ids] (group-by first tag+ids) 172 | cache (caches-by-tag tag)] 173 | (.compute 174 | cache->ids 175 | cache 176 | (reify BiFunction 177 | (apply [this k v] (into (or v []) tag+ids))))) 178 | tag (LockoutTag.)] 179 | (try 180 | (.startLockout base/lockout-map tag+ids tag) 181 | (run! (fn [e] (base/invalidate-ids (key e) (val e))) cache->ids) 182 | (finally 183 | (.endLockout base/lockout-map tag+ids tag))))) 184 | 185 | (defn memo-clear-tag! 186 | "Invalidate all entries that have the specified tag + id metadata. ID can be anything." 187 | [tag id] 188 | (memo-clear-tags! [tag id])) 189 | 190 | (defn update-tag-caches! 191 | "For each memoized function with the specified tag, set the Cache used by the fn to (cache-fn current-cache). 192 | 193 | Cache update function is ran on each 194 | memoized function (mount point), so if one cache is backing multiple functions, the cache update function is called 195 | multiple times on it. If you want to run cache-fn one each Cache instance only once, I recommend wrapping it 196 | in clojure.core/memoize. 197 | 198 | If caches are thread-bound to a different value with with-caches, then those 199 | bindings are modified instead of root bindings." 200 | [tag cache-fn] 201 | (mount/alter-caches-mapping tag mount/update-existing cache-fn)) 202 | 203 | (defmacro with-caches 204 | "Within the block, each memoized function with the specified tag has its cache update by cache-fn. 205 | 206 | The values are bound within the block as a thread local binding. Cache update function is ran on each 207 | memoized function (mount point), so if one cache is backing multiple functions, the cache update function is called 208 | multiple timed on it. If you want to run cache-fn one each Cache instance only once, I recommend wrapping it 209 | in clojure.core/memoize." 210 | [tag cache-fn & body] 211 | `(binding [mount/*caches* (mount/update-existing mount/*caches* (get @mount/tags ~tag []) ~cache-fn)] 212 | ~@body)) 213 | 214 | (defn evt-cache-add 215 | "Convenience function. It creates or wraps event handler fn, 216 | with an implementation which expects an event to be a vector of 217 | [event-type payload], it checks for matching event type and inserts 218 | the result of (->entries payload) into the cache." 219 | ([evt-type ->entries] (evt-cache-add (constantly nil) evt-type ->entries)) 220 | ([evt-fn evt-type ->entries] 221 | (fn [mountp evt] 222 | (when (and (vector? evt) 223 | (= (count evt) 2) 224 | (= evt-type (first evt))) 225 | (memo-add! mountp (->entries (second evt)))) 226 | (evt-fn mountp evt)))) 227 | 228 | (defn tiered 229 | "Creates a configuration for a tiered cache. Both parameters are either a conf map or a cache. 230 | 231 | Entry is fetched from cache, delegating to upstream is not found. After the operation 232 | the entry is in both caches. 233 | 234 | Useful when upstream is a big cache that outside the JVM, but it's not that inexpensive, so you 235 | want a local smaller cache in front of it. 236 | 237 | Invalidation operations also affect upstream. Other operations only affect local cache." 238 | [cache upstream] 239 | {::type ::tiered 240 | ::multi/cache cache 241 | ::multi/upstream upstream}) 242 | 243 | (defn consulting 244 | "Creates a configuration for a consulting tiered cache. Both parameters are either a conf map or a cache. 245 | 246 | Entry is fetched from cache, if not found, the upstream is asked for entry if present (but not to make one 247 | in the upstream). 248 | 249 | After the operation, the entry is in local cache, upstream is unchanged. 250 | 251 | Useful when you want to consult a long term upstream cache for existing entries, but you don't want any 252 | entries being created for the short term cache to be pushed upstream. 253 | 254 | Invalidation operations also affect upstream. Other operations only affect local cache." 255 | [cache upstream] 256 | {::type ::consulting 257 | ::multi/cache cache 258 | ::multi/upstream upstream}) 259 | 260 | (defn daisy 261 | "Creates a configuration for a daisy chained cache. Cache parameter is a conf map or a cache. 262 | 263 | Entry is returned from cache IF PRESENT, otherwise upstream is hit. The returned value 264 | is NOT added to cache. 265 | 266 | After the operation the entry is either in local or upstream cache. 267 | 268 | Useful when you don't want entries from upstream accumulating in local 269 | cache, and you're feeding the local cache via some other means: 270 | - a preloaded fixed cache 271 | - manually adding entries 272 | 273 | Invalidation operations also affect upstream. Other operations only affect local cache." 274 | [cache upstream] 275 | {::type ::daisy 276 | ::multi/cache cache 277 | ::multi/upstream upstream}) 278 | 279 | (defmacro if-cached 280 | "Like if-let, but then clause is executed if the call in the binding is cached, with the binding symbol 281 | being bound to the cached value. 282 | 283 | This assumes that the top form in bindings is a call of cached function, generating an error otherwise. 284 | 285 | e.g. (if-cached [my-val (my-cached-fn arg1)] ...)" 286 | ([bindings then] 287 | `(if-cached ~bindings ~then nil)) 288 | ([bindings then else] 289 | (assert (vector? bindings)) 290 | (assert (= 2 (count bindings))) 291 | (let [form (bindings 0) 292 | cache-call (bindings 1) 293 | _ (assert (list? cache-call)) 294 | f (first cache-call) 295 | _ (assert (symbol? f))] 296 | `(if-let [mnt# (mount/mount-point ~(first cache-call))] 297 | (let [mnt# (if (instance? Cached mnt#) (.getMp mnt#) mnt#) 298 | cached# (.ifCached mnt# '~(next cache-call))] 299 | (if (= cached# base/absent) 300 | ~else 301 | (let [~form cached#] ~then))) 302 | (throw (ex-info (str "Function " ~(str f) " is not a cached function") 303 | {:form '~cache-call})))))) 304 | -------------------------------------------------------------------------------- /java/memento/multi/ConsultingCache.java: -------------------------------------------------------------------------------- 1 | package memento.multi; 2 | 3 | import clojure.lang.*; 4 | import memento.base.ICache; 5 | import memento.base.Segment; 6 | 7 | public class ConsultingCache extends MultiCache { 8 | public ConsultingCache(ICache cache, ICache upstream, IPersistentMap conf, Object absent) { 9 | super(cache, upstream, conf, absent); 10 | } 11 | 12 | @Override 13 | public Object cached(Segment segment, ISeq args) { 14 | return cache.cached(segment.withFn(new UpstreamOrCalc(segment)), args); 15 | } 16 | 17 | private class UpstreamOrCalc implements IFn { 18 | 19 | private Segment segment; 20 | 21 | public UpstreamOrCalc(Segment segment) { 22 | this.segment = segment; 23 | } 24 | 25 | @Override 26 | public Object call() { 27 | ISeq s = ArraySeq.create(); 28 | Object up = upstream.ifCached(segment, s); 29 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 30 | } 31 | 32 | @Override 33 | public void run() { 34 | ISeq s = ArraySeq.create(); 35 | Object up = upstream.ifCached(segment, s); 36 | if (up == absent) { 37 | AFn.applyToHelper(segment.getF(), s); 38 | } 39 | } 40 | 41 | @Override 42 | public Object invoke() { 43 | ISeq s = ArraySeq.create(); 44 | Object up = upstream.ifCached(segment, s); 45 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 46 | } 47 | 48 | @Override 49 | public Object invoke(Object arg1) { 50 | ISeq s = ArraySeq.create(arg1); 51 | Object up = upstream.ifCached(segment, s); 52 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 53 | } 54 | 55 | @Override 56 | public Object invoke(Object arg1, Object arg2) { 57 | ISeq s = ArraySeq.create(arg1, arg2); 58 | Object up = upstream.ifCached(segment, s); 59 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 60 | } 61 | 62 | @Override 63 | public Object invoke(Object arg1, Object arg2, Object arg3) { 64 | ISeq s = ArraySeq.create(arg1, arg2, arg3); 65 | Object up = upstream.ifCached(segment, s); 66 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 67 | } 68 | 69 | @Override 70 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 71 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4); 72 | Object up = upstream.ifCached(segment, s); 73 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 74 | } 75 | 76 | @Override 77 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 78 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5); 79 | Object up = upstream.ifCached(segment, s); 80 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 81 | } 82 | 83 | @Override 84 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 85 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6); 86 | Object up = upstream.ifCached(segment, s); 87 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 88 | } 89 | 90 | @Override 91 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 92 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7); 93 | Object up = upstream.ifCached(segment, s); 94 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 95 | } 96 | 97 | @Override 98 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 99 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); 100 | Object up = upstream.ifCached(segment, s); 101 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 102 | } 103 | 104 | @Override 105 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 106 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); 107 | Object up = upstream.ifCached(segment, s); 108 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 109 | } 110 | 111 | @Override 112 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 113 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10); 114 | Object up = upstream.ifCached(segment, s); 115 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 116 | } 117 | 118 | @Override 119 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 120 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11); 121 | Object up = upstream.ifCached(segment, s); 122 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 123 | } 124 | 125 | @Override 126 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 127 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12); 128 | Object up = upstream.ifCached(segment, s); 129 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 130 | } 131 | 132 | @Override 133 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 134 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13); 135 | Object up = upstream.ifCached(segment, s); 136 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 137 | } 138 | 139 | @Override 140 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 141 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14); 142 | Object up = upstream.ifCached(segment, s); 143 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 144 | } 145 | 146 | @Override 147 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 148 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15); 149 | Object up = upstream.ifCached(segment, s); 150 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 151 | } 152 | 153 | @Override 154 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 155 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16); 156 | Object up = upstream.ifCached(segment, s); 157 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 158 | } 159 | 160 | @Override 161 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 162 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17); 163 | Object up = upstream.ifCached(segment, s); 164 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 165 | } 166 | 167 | @Override 168 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 169 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18); 170 | Object up = upstream.ifCached(segment, s); 171 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 172 | } 173 | 174 | @Override 175 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 176 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19); 177 | Object up = upstream.ifCached(segment, s); 178 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 179 | } 180 | 181 | @Override 182 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 183 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20); 184 | Object up = upstream.ifCached(segment, s); 185 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 186 | } 187 | 188 | @Override 189 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 190 | Object[] allArgs = new Object[20 + args.length]; 191 | System.arraycopy(args, 0, allArgs, 20, args.length); 192 | allArgs[0] = arg1; 193 | allArgs[1] = arg2; 194 | allArgs[2] = arg3; 195 | allArgs[3] = arg4; 196 | allArgs[4] = arg5; 197 | allArgs[5] = arg6; 198 | allArgs[6] = arg7; 199 | allArgs[7] = arg8; 200 | allArgs[8] = arg9; 201 | allArgs[9] = arg10; 202 | allArgs[10] = arg11; 203 | allArgs[11] = arg12; 204 | allArgs[12] = arg13; 205 | allArgs[13] = arg14; 206 | allArgs[14] = arg15; 207 | allArgs[15] = arg16; 208 | allArgs[16] = arg17; 209 | allArgs[17] = arg18; 210 | allArgs[18] = arg19; 211 | allArgs[19] = arg20; 212 | ISeq s = ArraySeq.create(allArgs); 213 | Object up = upstream.ifCached(segment, s); 214 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 215 | } 216 | 217 | @Override 218 | public Object applyTo(ISeq arglist) { 219 | Object up = upstream.ifCached(segment, arglist); 220 | return up == absent ? AFn.applyToHelper(segment.getF(), arglist) : up; 221 | } 222 | 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /java/memento/mount/CachedMultiFn.java: -------------------------------------------------------------------------------- 1 | package memento.mount; 2 | 3 | import clojure.lang.*; 4 | import memento.base.ICache; 5 | import memento.base.Segment; 6 | 7 | public class CachedMultiFn extends MultiFn implements IMountPoint, Cached, IObj { 8 | private final Object reloadGuard; 9 | private final MultiFn originalFn; 10 | private final Segment segment; 11 | 12 | @Override 13 | public IPersistentMap asMap() { 14 | return mp.asMap(); 15 | } 16 | 17 | @Override 18 | public Object cached(ISeq args) { 19 | return mp.cached(args); 20 | } 21 | 22 | @Override 23 | public Object ifCached(ISeq args) { 24 | return mp.ifCached(args); 25 | } 26 | 27 | @Override 28 | public Object getTags() { 29 | return mp.getTags(); 30 | } 31 | 32 | @Override 33 | public Object handleEvent(Object event) { 34 | return mp.handleEvent(event); 35 | } 36 | 37 | @Override 38 | public ICache invalidate(ISeq args) { 39 | return mp.invalidate(args); 40 | } 41 | 42 | @Override 43 | public ICache invalidateAll() { 44 | return mp.invalidateAll(); 45 | } 46 | 47 | @Override 48 | public ICache mountedCache() { 49 | return mp.mountedCache(); 50 | } 51 | 52 | @Override 53 | public Segment segment() { 54 | return mp.segment(); 55 | } 56 | 57 | @Override 58 | public ICache addEntries(IPersistentMap argsToVals) { 59 | return mp.addEntries(argsToVals); 60 | } 61 | 62 | private final String name; 63 | private final IMountPoint mp; 64 | private final IPersistentMap meta; 65 | 66 | public CachedMultiFn(String name, Object reloadGuard, IMountPoint mp, IPersistentMap meta, MultiFn originalFn) { 67 | super(name, originalFn.dispatchFn, originalFn.defaultDispatchVal, originalFn.hierarchy); 68 | this.reloadGuard = reloadGuard; 69 | this.mp = mp; 70 | this.name = name; 71 | this.meta = meta; 72 | this.originalFn = originalFn; 73 | this.segment = mp.segment(); 74 | } 75 | 76 | @Override 77 | public IPersistentMap meta() { 78 | return meta; 79 | } 80 | 81 | @Override 82 | public IObj withMeta(IPersistentMap meta) { 83 | return new CachedMultiFn(name, reloadGuard, mp, meta, originalFn); 84 | } 85 | 86 | @Override 87 | public Object call() { 88 | return mp.mountedCache().cached(segment, ArraySeq.create()); 89 | } 90 | 91 | @Override 92 | public void run() { 93 | mp.mountedCache().cached(segment, ArraySeq.create()); 94 | } 95 | 96 | @Override 97 | public Object invoke() { 98 | return mp.mountedCache().cached(segment, ArraySeq.create()); 99 | } 100 | 101 | @Override 102 | public Object invoke(Object arg1) { 103 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1)); 104 | } 105 | 106 | @Override 107 | public Object invoke(Object arg1, Object arg2) { 108 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2)); 109 | } 110 | 111 | @Override 112 | public Object invoke(Object arg1, Object arg2, Object arg3) { 113 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3)); 114 | } 115 | 116 | @Override 117 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 118 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4)); 119 | } 120 | 121 | @Override 122 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 123 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5)); 124 | } 125 | 126 | @Override 127 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 128 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6)); 129 | } 130 | 131 | @Override 132 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 133 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7)); 134 | } 135 | 136 | @Override 137 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 138 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)); 139 | } 140 | 141 | @Override 142 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 143 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)); 144 | } 145 | 146 | @Override 147 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 148 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); 149 | } 150 | 151 | @Override 152 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 153 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11)); 154 | } 155 | 156 | @Override 157 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 158 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12)); 159 | } 160 | 161 | @Override 162 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 163 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13)); 164 | } 165 | 166 | @Override 167 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 168 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14)); 169 | } 170 | 171 | @Override 172 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 173 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15)); 174 | } 175 | 176 | @Override 177 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 178 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16)); 179 | } 180 | 181 | @Override 182 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 183 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17)); 184 | } 185 | 186 | @Override 187 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 188 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18)); 189 | } 190 | 191 | @Override 192 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 193 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19)); 194 | } 195 | 196 | @Override 197 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 198 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20)); 199 | } 200 | 201 | @Override 202 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 203 | Object[] allArgs = new Object[20 + args.length]; 204 | System.arraycopy(args, 0, allArgs, 20, args.length); 205 | allArgs[0] = arg1; 206 | allArgs[1] = arg2; 207 | allArgs[2] = arg3; 208 | allArgs[3] = arg4; 209 | allArgs[4] = arg5; 210 | allArgs[5] = arg6; 211 | allArgs[6] = arg7; 212 | allArgs[7] = arg8; 213 | allArgs[8] = arg9; 214 | allArgs[9] = arg10; 215 | allArgs[10] = arg11; 216 | allArgs[11] = arg12; 217 | allArgs[12] = arg13; 218 | allArgs[13] = arg14; 219 | allArgs[14] = arg15; 220 | allArgs[15] = arg16; 221 | allArgs[16] = arg17; 222 | allArgs[17] = arg18; 223 | allArgs[18] = arg19; 224 | allArgs[19] = arg20; 225 | return mp.mountedCache().cached(segment, ArraySeq.create(allArgs)); 226 | } 227 | 228 | @Override 229 | public Object applyTo(ISeq arglist) { 230 | return mp.mountedCache().cached(segment, arglist); 231 | } 232 | 233 | public IMountPoint getMp() { 234 | return mp; 235 | } 236 | 237 | public IFn getOriginalFn() { 238 | return originalFn; 239 | } 240 | 241 | @Override 242 | public String toString() { 243 | return "CachedMultiFn{" + 244 | "originalFn=" + originalFn + 245 | ", segment=" + segment + 246 | ", mp=" + mp + 247 | ", meta=" + meta + 248 | '}'; 249 | } 250 | 251 | public Segment getSegment() { 252 | return segment; 253 | } 254 | 255 | @Override 256 | public MultiFn addMethod(Object dispatchVal, IFn method) { 257 | originalFn.addMethod(dispatchVal, method); 258 | return this; 259 | } 260 | 261 | @Override 262 | public IFn getMethod(Object dispatchVal) { 263 | return originalFn.getMethod(dispatchVal); 264 | } 265 | 266 | @Override 267 | public IPersistentMap getMethodTable() { 268 | return originalFn == null ? PersistentHashMap.EMPTY : originalFn.getMethodTable(); 269 | } 270 | 271 | @Override 272 | public IPersistentMap getPreferTable() { 273 | return originalFn.getPreferTable(); 274 | } 275 | 276 | @Override 277 | public MultiFn preferMethod(Object dispatchValX, Object dispatchValY) { 278 | originalFn.preferMethod(dispatchValX, dispatchValY); 279 | return this; 280 | } 281 | 282 | @Override 283 | public MultiFn removeMethod(Object dispatchVal) { 284 | originalFn.removeMethod(dispatchVal); 285 | return this; 286 | } 287 | 288 | @Override 289 | public MultiFn reset() { 290 | originalFn.reset(); 291 | return this; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Memento 2 | 3 | A library for function memoization with scoped caches and tagged eviction capabilities. 4 | 5 | ## Dependency 6 | 7 | [](https://clojars.org/org.clojars.roklenarcic/memento) 8 | 9 | ## Version 2.0 breaking changes 10 | 11 | Version 2 moves from Java 8 to Java 11 as minimum JVM version. Caffeine version is 3 instead of 2. 12 | 13 | ## Version 1.0 breaking changes 14 | 15 | Version 1.0 represents a switch from Guava to Caffeine, which is a faster caching library, with added 16 | benefit of not pulling in the whole Guava artefact which is more that just that Cache. The Guava Cache type 17 | key and the config namespace are deprecated and will be removed in the future. 18 | 19 | ## Motivation 20 | 21 | Why is there a need for another caching library? 22 | 23 | - request scoped caching (and other scoped caching) 24 | - eviction by secondary index 25 | - disabling cache for specific function returns 26 | - tiered caching 27 | - size based eviction that puts limits around more than one function at the time 28 | - cache events 29 | 30 | ## Performance 31 | 32 | - [**Performance**](doc/performance.md) 33 | 34 | ## Adding cache to a function 35 | 36 | **With require `[memento.core :as m][memento.config :as mc]`:** 37 | 38 | Define a function + create new cache + attach cache to a function: 39 | 40 | ```clojure 41 | (m/defmemo my-function 42 | {::m/cache {mc/type mc/caffeine}} 43 | [x] 44 | (* 2 x)) 45 | ``` 46 | 47 | ### **The key parts here**: 48 | - `defmemo` works just like `defn` but wraps the function in a cache 49 | - specify the cache configuration via `:memento.core/cache` keyword in function meta 50 | 51 | Quick reminder, there are two ways to provide metadata when defining functions: `defn` allows a meta 52 | map to be provided before the argument list, or you can add meta to the symbol directly as supported by the reader: 53 | 54 | ```clojure 55 | (m/defmemo ^{::m/cache {mc/type mc/caffeine}} my-function 56 | [x] 57 | (* 2 x)) 58 | ``` 59 | 60 | ### Caching an anonymous function 61 | 62 | You can add cache to a function object (in `clojure.core/memoize` fashion): 63 | 64 | ```clojure 65 | (m/memo (fn [] ...) {mc/type mc/caffeine}) 66 | ``` 67 | 68 | ### Other ways to attach Cache to a function 69 | 70 | [Caches and memoize calls](doc/major.md) 71 | 72 | ## Cache conf(iguration) 73 | 74 | See above: `{mc/type mc/caffeine}` 75 | 76 | The cache conf is an open map of namespaced keywords such as `:memento.core/type`, various cache implementations can 77 | use implementation specific config keywords. 78 | 79 | Learning all the keywords and what they do can be hard. To assist you 80 | there are special conf namespaces provided where conf keywords are defined as vars with docs, 81 | so it's easy so you to see which configuration keys are available and what their function is. It also helps 82 | prevent bugs from typing errors. 83 | 84 | The core properties are defined in `[memento.config :as mc]` namespace. Caffeine specific properties are defined 85 | in `[memento.caffeine.config :as mcc]`. 86 | 87 | Here's a couple of equal ways of writing out you cache configuration meta: 88 | 89 | ```clojure 90 | ; the longest 91 | {:memento.core/cache {:memento.core/type :memento.core/caffeine}} 92 | ; using alias 93 | {::m/cache {::m/type ::m/caffeine}} 94 | ; using memento.config vars - recommended 95 | {mc/cache {mc/type mc/caffeine}} 96 | ``` 97 | 98 | ### Core conf 99 | 100 | The core configuration properties: 101 | 102 | #### mc/type 103 | 104 | Cache implementation type, e.g. caffeine, redis, see the implementation library docs. **Make sure 105 | you load the implementation namespace at some point!**. Caffeine namespace is loaded automatically 106 | when memento.core is loaded. 107 | 108 | #### mc/size< 109 | 110 | Size limit expressed in number of entries or total weight if implementation supports weighted cache entries 111 | 112 | #### mc/ttl 113 | 114 | Entry is invalid after this amount of time has passed since its creation 115 | 116 | It's either a number (of seconds), a pair describing duration e.g. `[10 :m]` for 10 minutes, 117 | see `memento.config/timeunits` for timeunits. 118 | 119 | #### mc/fade 120 | 121 | Entry is invalid after this amount of time has passed since last access, see `mc/ttl` for duration 122 | specification. 123 | 124 | #### mc/key-fn, mc/key-fn* 125 | 126 | Specify a function that will transform the function arg list into the final cache key. Used 127 | to drop function arguments that shouldn't factor into cache tag equality. 128 | 129 | The `key-fn` receives a sequence of arguments, `key-fn*` receives multiple arguments as if it 130 | was the function itself. 131 | 132 | See: [Changing the key for cached tag](doc/key-fn.md) 133 | 134 | #### mc/ret-fn 135 | 136 | A function that is called on every cached function return value. Used for general transformations 137 | of return values. 138 | 139 | #### mc/ret-ex-fn 140 | 141 | A function that is called on every thrown Throwable. Used for general transformations 142 | of thrown exceptions values. 143 | 144 | #### mc/seed 145 | 146 | Initial entries to load in the cache. 147 | 148 | #### mc/initial-capacity 149 | 150 | Cache capacity hint to implementation. 151 | 152 | ## Conf is a value (map) 153 | 154 | Cache conf can get quite involved: 155 | 156 | ```clojure 157 | (ns memento.tryout 158 | (:require [memento.core :as m] 159 | ; general cache conf keys 160 | [memento.config :as mc] 161 | ; caffeine specific cache conf keys 162 | [memento.caffeine.config :as mcc])) 163 | 164 | (def my-weird-cache 165 | "Conf for caffeine cache that caches up to 20 seconds and up to 30 entries, uses weak 166 | references and prints when keys get evicted." 167 | {mc/type mc/caffeine 168 | mc/size< 30 169 | mc/ttl 20 170 | mcc/weak-values true 171 | mcc/removal-listener #(println (apply format "Function %s key %s, value %s got evicted because of %s" %&))}) 172 | 173 | (m/defmemo my-function 174 | {::m/cache my-weird-cache} 175 | [x] (* 2 x)) 176 | ``` 177 | 178 | Seeing as cache conf is a map, I recommend a pattern where you have a namespace in your application that contains vars 179 | with your commonly used cache conf maps and functions that generate slightly parameterized 180 | configuration. E.g. 181 | 182 | ```clojure 183 | (ns my-project.cache 184 | (:require [memento.config :as mc])) 185 | 186 | ;; infinite cache 187 | (def inf-cache {mc/type mc/caffeine}) 188 | 189 | (defn for-seconds [n] (assoc inf-cache mc/ttl n)) 190 | ``` 191 | 192 | Then you just use that in your code: 193 | 194 | ```clojure 195 | (m/defmemo my-function 196 | {::m/cache (cache/for-seconds 60)} 197 | [x] (* x 2)) 198 | ``` 199 | 200 | ## Caches and mount points 201 | 202 | Enabling memoization of a function is composed of two distinct steps: 203 | 204 | - creating a Cache (optional, as you can use an existing cache) 205 | - binding the cache to the function (a MountPoint is used to connect a function being memoized to the cache) 206 | 207 | A cache, an instance of memento.base/Cache, can contain entries from multiple functions and can be shared between memoized functions. 208 | Each memoized function is bound to a Cache via MountPoint. When you call a function such as `(m/as-map a-cached-function)` you are 209 | operating on a MountPoint. 210 | 211 | The reason for this separation is two-fold: 212 | 213 | #### 1. **Improved Size Based Eviction** 214 | 215 | So far all examples implicitly created a new cache for each memoized function, but if we use same cache for multiple 216 | functions, then any size based eviction will apply to them as a whole. If you have 100 memoized functions, and you want to 217 | somewhat limit their memory use, what do you do? In a typical cache library you might limit each of them to 100 entries. So you 218 | allocated 10000 slots total, but one function might have an empty cache, while a very heavily used one needs way more than 100 219 | slots. If all 100 function are backed by same Cache instance with 10000 slots then they automatically balance themselves out. 220 | 221 | #### 2. **Changing cache temporarily to allow for scoped caching** 222 | 223 | This indirection with Mount Points allows us to change which cache is backing a function dynamically. See discussion of tagged 224 | caches below. Here's an example of using tags when caching and scoped caching 225 | 226 | ```clojure 227 | (ns myproject.some-ns 228 | (:require [myproject.cache :as cache] 229 | [memento.core :as m])) 230 | 231 | (defn get-person-by-id [person-id] 232 | (let [person (db/get-person person-id)] 233 | ; tag the returned object with :person + id pair 234 | (m/with-tag-id person :person (:id person)))) 235 | 236 | ; add a cache to the function with tags :person and :request 237 | (m/memo #'get-person-by-id [:person :request] cache/inf) 238 | 239 | ; remove cache entries from every cache tagged :person globally, where the 240 | ; tag is tagged with :person 1 241 | (m/memo-clear-tag! :person 1) 242 | 243 | (m/with-caches :request (constantly (m/create cache/inf)) 244 | ; inside this block, a fresh new cache is used (and discarded) 245 | ; making a scope-like functionality 246 | (get-person-by-id 5)) 247 | ``` 248 | 249 | ## Variable expiry 250 | 251 | Instead of setting a fixed duration of validity for entries in a cache, it is possible 252 | to set these duration on per-tag or per-mount point basis. 253 | 254 | Note that for Caffeine cache variable expiry caching is somewhat slower. 255 | 256 | ### **Read [here](doc/variable-expiry.md)** 257 | 258 | ## Additional features 259 | 260 | #### [Prevent caching of a specific return value (and general return value xform)](doc/ret-fn.md) 261 | #### [Manually add or evict entries](doc/manual-add-remove.md) 262 | 263 | #### `(m/as-map memoized-function)` to get a map of cache entries, also works on MountPoint instances 264 | #### `(m/memoized? a-function)` returns true if the function is memoized 265 | #### `(m/memo-unwrap memoized-function)` returns original uncached function, also works on MountPoint instances 266 | #### `(m/active-cache memoized-function)` returns Cache instance from the function, if present. 267 | 268 | ## Tags 269 | 270 | You can add tags to the caches. Tags enable that you: 271 | 272 | - run actions on caches with specific tags 273 | - **change or update cache of tagged MountPoints within a scope** 274 | - change or update cache of tagged MountPoints permanently 275 | - use secondary index to invalidate entries by a tag + ID pair 276 | 277 | This is a very powerful feature, [read more here.](doc/tags.md) 278 | 279 | ## Loads and invalidations 280 | 281 | Cache only has a single ongoing load for a key going at any one time. For Caffeine cache, if a key is invalidated 282 | during the load, the load is repeated. This is the only way you can get multiple function invocations happen for a single 283 | cached function call. When an tag is invalidated while it's being loaded, the Thread that loads it will be interrupted. 284 | 285 | ## Namespace scan 286 | 287 | You can scan loaded namespaces for annotated vars and automatically create caches. 288 | 289 | [Read more](doc/ns-scan.md) 290 | 291 | ## Events 292 | 293 | You can fire an event at a memoized function. Main use case is to enable adding entries to different functions from same data. 294 | 295 | [Read more](doc/events.md) 296 | 297 | ## Tiered caching 298 | 299 | You can use caches that combine two other caches in some way. The easiest way to generate 300 | the cache configuration needed is to use `memento.core/tiered`,`memento.core/consulting`, `memento.core/daisy`. 301 | 302 | [Read more](doc/tiered.md) 303 | 304 | ## if-cached 305 | 306 | memento.core/if-cache is like an if-let, but the "then" branch executes if the function call 307 | is cached, otherwise else branch is executed. The binding is expected to be a cached function call form, otherwise 308 | an error is thrown. 309 | 310 | Example: 311 | 312 | ```clojure 313 | (if-cached [v (my-function arg1)] 314 | (println "cached value is " v) 315 | (println "value is not cached")) 316 | ``` 317 | 318 | ## Skip/disable caching 319 | 320 | If you set `-Dmemento.enabled=false` JVM option (or change `memento.config/enabled?` var root binding), 321 | then type of all caches created will be `memento.base/no-cache`, which does no caching. 322 | 323 | ## Reload guards 324 | 325 | When you memoize a function with tags, a special object is created that will clean up in internal tag 326 | mappings when memoized function is GCed. It's important when reloading namespaces to remove mount points 327 | on the old function versions. 328 | 329 | It uses finalize, which isn't free (takes extra work to allocate and GC has to work harder), so 330 | if you don't use namespace reloading, and you want to optimize you can disable reload guard objects. 331 | 332 | Set `-Dmemento.reloadable=false` JVM option (or change `memento.config/reload-guards?` var root binding). 333 | 334 | ## Breaking changes 335 | 336 | Patch versions are compatible. Minor version change breaks API for implementation authors, but not for users, 337 | major version change breaks user API. 338 | 339 | Version 1.0.x changed implementation from Guava to Caffeine 340 | Version 0.9.0 introduced many breaking changes. 341 | 342 | ## License 343 | 344 | Copyright © 2020-2021 Rok Lenarčič 345 | 346 | Licensed under the term of the MIT License, see LICENSE. 347 | -------------------------------------------------------------------------------- /test/memento/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns memento.core-test 2 | (:require [clojure.test :refer :all] 3 | [memento.core :as m :refer :all] 4 | [memento.config :as mc] 5 | [memento.caffeine.config :as mcc]) 6 | (:import (java.io IOException) 7 | (memento.base EntryMeta ICache) 8 | (memento.caffeine Expiry) 9 | (memento.mount IMountPoint))) 10 | 11 | (def inf {mc/type mc/caffeine}) 12 | (defn size< [max-size] 13 | (assoc inf mc/size< max-size)) 14 | (defn ret-fn [f] 15 | (assoc inf mc/ret-fn f)) 16 | 17 | (def id (memo identity inf)) 18 | 19 | (defn- check-core-features 20 | [factory] 21 | (let [mine (factory identity) 22 | them (memoize identity)] 23 | (testing "That the memo function works the same as core.memoize" 24 | (are [x y] (= x y) 25 | (mine 42) (them 42) 26 | (mine ()) (them ()) 27 | (mine []) (them []) 28 | (mine #{}) (them #{}) 29 | (mine {}) (them {}) 30 | (mine nil) (them nil))) 31 | (testing "That the memo function has a proper cache" 32 | (is (memoized? mine)) 33 | (is (not (memoized? them))) 34 | (is (= 42 (mine 42))) 35 | (is (not (empty? (into {} (as-map mine))))) 36 | (is (memo-clear! mine)) 37 | (is (empty? (into {} (as-map mine)))))) 38 | (testing "That the cache retries in case of exceptions" 39 | (let [access-count (atom 0) 40 | f (factory 41 | (fn [] 42 | (swap! access-count inc) 43 | (throw (IllegalArgumentException.))))] 44 | (is (thrown? IllegalArgumentException (f))) 45 | (is (thrown? IllegalArgumentException (f))) 46 | (is (= 2 @access-count)))) 47 | (testing "That the memo function does not have a race condition" 48 | (let [access-count (atom 0) 49 | slow-identity 50 | (factory (fn [x] 51 | (swap! access-count inc) 52 | (Thread/sleep 100) 53 | x))] 54 | (every? identity (pvalues (slow-identity 5) (slow-identity 5))) 55 | (is (= @access-count 1)))) 56 | (testing "That exceptions are correctly unwrapped." 57 | (is (thrown? ClassNotFoundException ((factory (fn [] (throw (ClassNotFoundException.))))))) 58 | (is (thrown? IllegalArgumentException ((factory (fn [] (throw (IllegalArgumentException.)))))))) 59 | (testing "Null return caching." 60 | (let [access-count (atom 0) 61 | mine (factory (fn [] (swap! access-count inc) nil))] 62 | (is (nil? (mine))) 63 | (is (nil? (mine))) 64 | (is (= @access-count 1))))) 65 | 66 | (deftest test-memo (check-core-features #(memo % inf))) 67 | 68 | (deftest test-lru 69 | (let [mine (memo identity (size< 2))] 70 | ;; First check that the basic memo behavior holds 71 | (check-core-features #(memo % (size< 2))) 72 | 73 | ;; Now check FIFO-specific behavior 74 | (testing "that when the limit threshold is not breached, the cache works like the basic version" 75 | (are [x y] = 76 | 42 (mine 42) 77 | {[42] 42} (as-map mine) 78 | 43 (mine 43) 79 | {[42] 42, [43] 43} (as-map mine) 80 | 42 (mine 42) 81 | {[42] 42, [43] 43} (as-map mine))) 82 | (testing "that when the limit is breached, the oldest value is dropped" 83 | (are [x y] = 84 | 44 (mine 44) 85 | {[44] 44, [43] 43} (as-map mine))))) 86 | 87 | 88 | (deftest test-ttl 89 | ;; First check that the basic memo behavior holds 90 | (check-core-features #(memo % (assoc inf mc/ttl 2))) 91 | 92 | ;; Now check TTL-specific behavior 93 | (let [mine (memo identity (assoc inf mc/ttl [2 :s]))] 94 | (are [x y] = 95 | 42 (mine 42) 96 | {[42] 42} (as-map mine)) 97 | (Thread/sleep 3000) 98 | (are [x y] = 99 | 43 (mine 43) 100 | {[43] 43} (as-map mine))) 101 | 102 | (let [mine (memo identity (assoc inf mc/ttl [5 :ms])) 103 | limit 2000000 104 | start (System/currentTimeMillis)] 105 | (loop [n 0] 106 | (if-not (mine 42) 107 | (do 108 | (is false (str "Failure on call " n))) 109 | (if (< n limit) 110 | (recur (+ 1 n))))) 111 | (println "ttl test completed" limit "calls in" 112 | (- (System/currentTimeMillis) start) "ms"))) 113 | 114 | (deftest test-memoization-utils 115 | (let [CACHE_IDENTITY (:memento.mount/mount (meta id))] 116 | (testing "that the stored cache is not null" 117 | (is (instance? IMountPoint id))) 118 | (testing "that a populated function looks correct at its inception" 119 | (is (memoized? id)) 120 | (is (instance? ICache (active-cache id))) 121 | (is (as-map id)) 122 | (is (empty? (as-map id)))) 123 | (testing "that a populated function looks correct after some interactions" 124 | ;; Memoize once 125 | (is (= 42 (id 42))) 126 | ;; Now check to see if it looks right. 127 | (is (find (as-map id) '(42))) 128 | (is (= 1 (count (as-map id)))) 129 | ;; Memoize again 130 | (is (= [] (id []))) 131 | (is (find (as-map id) '([]))) 132 | (is (= 2 (count (as-map id)))) 133 | (testing "that upon memoizing again, the cache should not change" 134 | (is (= [] (id []))) 135 | (is (find (as-map id) '([]))) 136 | (is (= 2 (count (as-map id))))) 137 | (testing "if clearing the cache works as expected" 138 | (is (memo-clear! id)) 139 | (is (empty? (as-map id))))) 140 | (testing "that after all manipulations, the cache maintains its identity" 141 | (is (identical? CACHE_IDENTITY (:memento.mount/mount (meta id))))) 142 | (testing "that a cache can be seeded and used normally" 143 | (memo-clear! id) 144 | (is (memo-add! id {[42] 42})) 145 | (is (= 42 (id 42))) 146 | (is (= {[42] 42} (as-map id))) 147 | (is (= 108 (id 108))) 148 | (is (= {[42] 42 [108] 108} (as-map id))) 149 | (is (memo-add! id {[111] nil [nil] 111})) 150 | (is (= 111 (id nil))) 151 | (is (= nil (id 111))) 152 | (is (= {[42] 42 [108] 108 [111] nil [nil] 111} (as-map id)))) 153 | (testing "that we can get back the original function" 154 | (is (memo-clear! id)) 155 | (is (memo-add! id {[42] 24})) 156 | (is (= 24 (id 42))) 157 | (is (= 42 ((memo-unwrap id) 42)))))) 158 | 159 | (deftest memo-with-seed-cmemoize-18 160 | (let [mine (memo identity (assoc inf mc/seed {[42] 99}))] 161 | (testing "that a memo seed works" 162 | (is (= 41 (mine 41))) 163 | (is (= 99 (mine 42))) 164 | (is (= 43 (mine 43))) 165 | (is (= {[41] 41, [42] 99, [43] 43} (as-map mine)))))) 166 | 167 | (deftest memo-with-dropped-args 168 | ;; must use var to preserve metadata 169 | (let [mine (memo + (assoc inf mc/key-fn rest))] 170 | (testing "that key-fnb collapses the cache key space" 171 | (is (= 13 (mine 1 2 10))) 172 | (is (= 13 (mine 10 2 1))) 173 | (is (= 13 (mine 10 2 10))) 174 | (is (= {[2 10] 13, [2 1] 13} (as-map mine)))))) 175 | 176 | (def test-atom (atom 0)) 177 | (defn test-var-fn [x] (swap! test-atom inc) (* x 3)) 178 | 179 | (deftest add-memo-to-var 180 | (testing "that memoing a var works" 181 | (memo #'test-var-fn inf) 182 | (is (= 3 (test-var-fn 1))) 183 | (is (= 3 (test-var-fn 1))) 184 | (is (= 3 (test-var-fn 1))) 185 | (is (= @test-atom 1)))) 186 | 187 | (deftest seed-test 188 | (testing "that seeding a function works" 189 | (let [cached (memo + (assoc inf mc/seed {[3 5] 100 [4 5] 2000}))] 190 | (is (= 50 (cached 20 30))) 191 | (is (= 1 (cached -1 2))) 192 | (is (= 100 (cached 3 5))) 193 | (is (= 2000 (cached 4 5)))))) 194 | 195 | (deftest key-fn-test 196 | (testing "that key-fn works for direct cache" 197 | (let [cached (memo (fn [& ids] ids) (assoc inf mc/key-fn set))] 198 | (is (= [3 2 1] (cached 3 2 1))) 199 | (is (= [3 2 1] (cached 1 2 3))) 200 | (is (= [3 2 1] (cached 1 3 3 2 2 2))) 201 | (is (= [2 1] (cached 2 1)))))) 202 | 203 | (deftest key-fn*-test 204 | (testing "that key-fn works for direct cache" 205 | (let [cached (memo (fn [& ids] ids) (assoc inf mc/key-fn* hash-set))] 206 | (is (= [3 2 1] (cached 3 2 1))) 207 | (is (= [3 2 1] (cached 1 2 3))) 208 | (is (= [3 2 1] (cached 1 3 3 2 2 2))) 209 | (is (= [2 1] (cached 2 1)))))) 210 | 211 | (deftest ret-fn-non-cached 212 | (testing "that ret-fn is ran" 213 | (is (= -4 ((memo + (ret-fn #(* -1 %2))) 2 2))) 214 | (is (= true ((memo (constantly nil) (ret-fn #(nil? %2))) 1))) 215 | (is (= nil ((memo + (ret-fn (constantly nil))) 2 2)))) 216 | (testing "that non-cached is respected" 217 | (let [access-nums (atom []) 218 | f (memo 219 | (fn [number] 220 | (swap! access-nums conj number) 221 | (if (zero? (mod number 3)) (do-not-cache number) number)) 222 | (ret-fn #(if (and (number? %2) (zero? (mod %2 5))) (do-not-cache %2) %2)))] 223 | (is (= (range 20) (map f (range 20)))) 224 | (is (= (range 20) (map f (range 20)))) 225 | (is (= (concat (range 20) [0 3 5 6 9 10 12 15 18]) @access-nums))))) 226 | 227 | (deftest get-tags-test 228 | (testing "tags get returned" 229 | (let [cached (memo identity :person) 230 | cached2 (memo identity [:actor :dog]) 231 | cached3 (memo identity {mc/tags :x})] 232 | (is (= [:person] (tags cached))) 233 | (is (= [:actor :dog] (tags cached2))) 234 | (is (= [:x] (tags cached3)))))) 235 | 236 | (deftest with-caches-test 237 | (testing "a different cache is used within the block" 238 | (let [access-nums (atom []) 239 | f (memo (fn [number] (swap! access-nums conj number)) :person inf)] 240 | (is (= [10] (f 10))) 241 | (is (= [10] (f 10))) 242 | (is (= [10 20] (f 20))) 243 | (is (= [10 20] (f 20))) 244 | (is (= [10 20] @access-nums)) 245 | (with-caches :person (constantly (create inf)) 246 | (is (= [10 20 10] (f 10))) 247 | (is (= [10 20 10] (f 10))) 248 | (is (= [10 20 10 30] (f 30))) 249 | (is (= [10 20 10 30] @access-nums))) 250 | (is (= [10] (f 10))) 251 | (is (= [10 20 10 30 30] (f 30)))))) 252 | 253 | (deftest update-tag-caches-test 254 | (testing "changes cache root binding" 255 | (let [access-nums (atom 0) 256 | f (memo (fn [number] (swap! access-nums + number)) :person inf)] 257 | (is (= 10 (f 10))) 258 | (is (= 10 (f 10))) 259 | (is (= 10 @access-nums)) 260 | (update-tag-caches! :person (constantly (create inf))) 261 | (is (= 20 (f 10))) 262 | (is (= 20 @access-nums)) 263 | (with-caches :person (constantly (create inf)) 264 | (is (= 30 (f 10))) 265 | (is (= 30 (f 10))) 266 | (is (= 30 @access-nums)) 267 | (update-tag-caches! :person (constantly (create inf))) 268 | (is (= 40 (f 10))) 269 | (is (= 40 @access-nums))) 270 | (is (= 20 (f 10))) 271 | (is (= 40 @access-nums)) 272 | (update-tag-caches! :person (constantly (create inf))) 273 | (is (= 50 (f 10))) 274 | (is (= 50 @access-nums))))) 275 | 276 | (deftest tagged-eviction-test 277 | (testing "adding tag ID info" 278 | (is (= (EntryMeta. 1 false #{[:person 55]}) 279 | (-> 1 (with-tag-id :person 55)))) 280 | (is (= (EntryMeta. 1 true #{[:person 55] [:account 6]}) 281 | (-> 1 (with-tag-id :person 55) (with-tag-id :account 6) do-not-cache)))) 282 | (testing "tagged eviction" 283 | (let [f (memo (fn [x] (with-tag-id x :tag x)) :tag inf)] 284 | (is (= {} (as-map f))) 285 | (is (= {[1] 1} (do (f 1) (as-map f)))) 286 | (is (= {[1] 1 [2] 2} (do (f 2) (as-map f)))) 287 | (is (= {[2] 2} (do (memo-clear-tag! :tag 1) (as-map f))))))) 288 | 289 | (deftest fire-event-test 290 | (testing "event is fired on referenced cache" 291 | (let [access-nums (atom 0) 292 | inner-f (fn [x] (swap! access-nums inc) x) 293 | evt-f (fn [this evt] 294 | (m/memo-add! this {[evt] (inc evt)})) 295 | x (m/memo inner-f {mc/type mc/caffeine mc/evt-fn evt-f mc/tags [:a]}) 296 | y (m/memo inner-f {mc/type mc/caffeine mc/evt-fn evt-f})] 297 | (is (= 1 (x 1))) 298 | (is (= 1 (x 1))) 299 | (is (= 1 @access-nums)) 300 | (m/fire-event! x 4) 301 | (m/fire-event! :a 5) 302 | (m/fire-event! y 6) 303 | (is (= {[1] 1 304 | [4] 5 305 | [5] 6} (m/as-map x))) 306 | (is (= {[6] 7} (m/as-map y))) 307 | (is (= 5 (x 4))) 308 | (is (= 6 (x 5))) 309 | (is (= 7 (y 6))) 310 | (is (= 1 @access-nums))))) 311 | 312 | (deftest if-cached-test 313 | (testing "if-cached executes then when cached" 314 | (let [x (m/memo identity {mc/type mc/caffeine})] 315 | (x 2) 316 | (is (= 2 317 | (m/if-cached [y (x 2)] 318 | y 319 | (throw (ex-info "Shouldn't throw" {}))))))) 320 | (testing "if-cached executes else when not cached" 321 | (let [x (m/memo identity {mc/type mc/caffeine})] 322 | (is (= ::none 323 | (m/if-cached [y (x 2)] 324 | (throw (ex-info "Shouldn't throw" {})) 325 | ::none)))))) 326 | 327 | (deftest put-during-load-test 328 | (testing "adding entries during load" 329 | (let [c (m/create inf) 330 | fn1 (m/memo identity {} c) 331 | fn2 (m/memo (fn [x] (m/memo-add! fn1 {[x] (inc x)}) 332 | (dec x)))] 333 | (is (= 4 (fn2 5))) 334 | (is (= 6 (fn1 5)))))) 335 | 336 | (defn fib [x] (if (<= x 1) 1 (+ (fib (- x 2)) (fib (dec x))))) 337 | 338 | (memo #'fib inf) 339 | 340 | (defn recursive [x] (recursive x)) 341 | 342 | (memo #'recursive inf) 343 | 344 | (deftest recursive-test 345 | (testing "recursive loads" 346 | (is (= 20365011074 (fib 50))) 347 | (is (thrown? StackOverflowError (recursive 1))))) 348 | 349 | (deftest concurrent-load 350 | (testing "concurrent test" 351 | (let [cnt (atom 0) 352 | f (m/memo (fn [x] 353 | (Thread/sleep 1000) 354 | (swap! cnt inc) x) 355 | inf) 356 | v (doall (repeatedly 5 #(future (f 1))))] 357 | (is (= [1 1 1 1 1] (mapv deref v)))))) 358 | 359 | (deftest vectors-key-fn* 360 | (testing "vectors don't throw exception when used with key-fn*" 361 | (let [c (m/memo identity (assoc inf mc/key-fn* identity))] 362 | (is (some? (m/memo-add! c {[1] 2})))))) 363 | 364 | (deftest invalidation-during-load-test 365 | (testing "bulk invalidation test" 366 | (let [a (atom 0) 367 | c (m/memo (fn [] (Thread/sleep 300) 368 | (m/with-tag-id (swap! a inc) :xx 1)) 369 | (assoc inf mc/tags :xx))] 370 | (future (Thread/sleep 15) 371 | (m/memo-clear-tag! :xx 1)) 372 | (is (= 2 (c))))) 373 | (testing "Invalidation during load test" 374 | (let [a (atom 0) 375 | after (atom 0) 376 | c (m/memo (fn [] (let [r (swap! a inc)] 377 | (Thread/sleep 300) 378 | [r (swap! after inc)])) inf)] 379 | (future (Thread/sleep 10) 380 | (m/memo-clear! c)) 381 | (is (= [2 1] (c)))))) 382 | 383 | (deftest ret-ex-fn-test 384 | (testing "returns transformed-exception" 385 | (let [e (RuntimeException.) 386 | c (m/memo (fn [] (Thread/sleep 100) 387 | (throw (IOException.))) 388 | (assoc inf mc/ret-ex-fn (fn [_ ee] (when (instance? IOException ee) e)))) 389 | f1 (future (try (c) (catch Exception e e))) 390 | f2 (future (try (c) (catch Exception e e)))] 391 | (is (= e @f1)) 392 | (is (= e @f2))))) 393 | 394 | (deftest variable-expiry-test 395 | (testing "Variable expiry" 396 | (let [c (m/memo 397 | identity 398 | (assoc inf mcc/expiry 399 | (reify Expiry 400 | (ttl [this _ k v] v) 401 | (fade [this _ k v]))))] 402 | (c 1) 403 | (c 2) 404 | (c 3) 405 | (Thread/sleep 1100) 406 | (is (= {'(2) 2 '(3) 3} (m/as-map c))))) 407 | (testing "Variable expiry fade" 408 | (let [c (m/memo 409 | identity 410 | (assoc inf mcc/expiry 411 | (reify Expiry 412 | (ttl [this _ k v] ) 413 | (fade [this _ k v] v))))] 414 | (c 1) 415 | (c 2) 416 | (c 3) 417 | (Thread/sleep 1100) 418 | (is (= {'(2) 2 '(3) 3} (m/as-map c))))) 419 | (testing "variable expiry via meta" 420 | (let [c (m/memo 421 | #(with-meta {} {mc/ttl (long (+ 1 %))}) 422 | (assoc inf mcc/expiry mcc/meta-expiry))] 423 | (c 1) 424 | (c 2) 425 | (c 3) 426 | (Thread/sleep 1100) 427 | (is (= {'(1) {} '(2) {} '(3) {}} (m/as-map c))) 428 | (Thread/sleep 1000) 429 | (is (= {'(2) {} '(3) {}} (m/as-map c)))))) --------------------------------------------------------------------------------
13 | * This class is NOT threadsafe. The intended use is as a faster CompletableFuture that is 14 | * aware of the thread that made it, so it can detect when same thread is trying to await on it 15 | * and throw to prevent deadlock. 16 | *
17 | * It is also expected that it is created and delivered by the same thread and it is expected that 18 | * any other thread is awaiting result and that only a single attempt is made at delivering the result. 19 | *
20 | * It does not include logic to deal with multiple calls to deliver the promise, as it's optimized for 21 | * a specific use case. 22 | */ 23 | public class SpecialPromise { 24 | 25 | private static final AltResult NIL = new AltResult(null); 26 | private final CountDownLatch d = new CountDownLatch(1); 27 | // these 2 don't need to be thread-safe, because they are only used to check 28 | // if current thread is one that created and started the load on the promise 29 | // so even with non-volatile, check is only true if thread is same as current thread 30 | // so no memory barrier needed 31 | private final HashSet invalidatedIds = new HashSet<>(); 32 | private volatile Thread thread; 33 | private volatile Object result; 34 | 35 | public void init() { 36 | this.thread = Thread.currentThread(); 37 | } 38 | 39 | public Object await(Object stackOverflowContext) throws Throwable { 40 | if (thread == Thread.currentThread()) { 41 | throw new StackOverflowError("Recursive load on key: " + stackOverflowContext); 42 | } 43 | Object r; 44 | if ((r = result) == null) { 45 | d.await(); 46 | r = result; 47 | } 48 | if (r instanceof AltResult) { 49 | Throwable x = ((AltResult) r).value; 50 | if (x == null) { 51 | return null; 52 | } else { 53 | throw x; 54 | } 55 | } else { 56 | return r; 57 | } 58 | } 59 | 60 | private boolean isLockedOut(EntryMeta em) { 61 | try { 62 | return LockoutMap.awaitLockout(em); 63 | } catch (InterruptedException e) { 64 | return true; 65 | } 66 | } 67 | 68 | // Returns true if delivered object is viable 69 | public boolean deliver(Object r) { 70 | if (r instanceof EntryMeta) { 71 | EntryMeta em = (EntryMeta) r; 72 | if (isLockedOut(em) || hasInvalidatedTagId(em)) { 73 | result = EntryMeta.absent; 74 | return false; 75 | } 76 | } 77 | if (result != EntryMeta.absent) { 78 | result = r == null ? NIL : r; 79 | return true; 80 | } 81 | return false; 82 | } 83 | 84 | public void deliverException(Throwable t) { 85 | result = new AltResult(t); 86 | } 87 | 88 | public Object getNow() throws Throwable { 89 | Object r; 90 | if (d.getCount() != 0) { 91 | return EntryMeta.absent; 92 | } 93 | if ((r = result) instanceof AltResult) { 94 | Throwable x = ((AltResult) r).value; 95 | if (x == null) { 96 | return null; 97 | } else { 98 | throw x; 99 | } 100 | } else { 101 | return r == null ? EntryMeta.absent : r; 102 | } 103 | } 104 | 105 | public void invalidate() { 106 | result = EntryMeta.absent; 107 | thread.interrupt(); 108 | } 109 | 110 | public boolean isInvalid() { 111 | return result == EntryMeta.absent; 112 | } 113 | 114 | public void releaseResult() { 115 | d.countDown(); 116 | } 117 | 118 | private boolean hasInvalidatedTagId(EntryMeta entryMeta) { 119 | synchronized (invalidatedIds) { 120 | ISeq s = entryMeta.getTagIdents().seq(); 121 | while (s != null) { 122 | if (invalidatedIds.contains(s.first())) { 123 | return true; 124 | } 125 | s = s.next(); 126 | } 127 | } 128 | return false; 129 | } 130 | 131 | public void addInvalidIds(Iterable ids) { 132 | synchronized (invalidatedIds) { 133 | for (Object id : ids) { 134 | invalidatedIds.add(id); 135 | } 136 | } 137 | } 138 | 139 | private static class AltResult { 140 | Throwable value; 141 | 142 | public AltResult(Throwable value) { 143 | this.value = value; 144 | } 145 | } 146 | 147 | } -------------------------------------------------------------------------------- /src/memento/caffeine.clj: -------------------------------------------------------------------------------- 1 | (ns memento.caffeine 2 | "Caffeine cache implementation." 3 | {:author "Rok Lenarčič"} 4 | (:require [memento.base :as b]) 5 | (:import (java.util.concurrent TimeUnit) 6 | (memento.base Durations CacheKey EntryMeta ICache Segment) 7 | (com.github.benmanes.caffeine.cache Caffeine Weigher Ticker) 8 | (memento.caffeine CaffeineCache_ SecondaryIndex SpecialPromise Expiry) 9 | (memento.mount IMountPoint))) 10 | 11 | (defn create-expiry 12 | "Assumes variable expiry is needed. So either ttl or fade is a function." 13 | [ttl fade ^Expiry cache-expiry] 14 | (let [read-default (some-> (or ttl fade) (Durations/nanos)) 15 | write-default (Durations/nanos (or ttl fade [Long/MAX_VALUE :ns]))] 16 | (reify com.github.benmanes.caffeine.cache.Expiry 17 | (expireAfterCreate [this k v current-time] 18 | (if (instance? SpecialPromise v) 19 | Long/MAX_VALUE 20 | (.expireAfterUpdate this k v current-time Long/MAX_VALUE))) 21 | (expireAfterUpdate [this k v current-time current-duration] 22 | (if-let [ret (.ttl cache-expiry {} (.getArgs ^CacheKey k) v)] 23 | (Durations/nanos ret) 24 | (if-let [ret (.fade cache-expiry {} (.getArgs ^CacheKey k) v)] 25 | (Durations/nanos ret) 26 | write-default))) 27 | (expireAfterRead [this k v current-time current-duration] 28 | (if (instance? SpecialPromise v) 29 | current-duration 30 | ;; if fade is not specified, keep current validity (probably set by ttl) 31 | (if-let [ret (.fade cache-expiry {} (.getArgs ^CacheKey k) v)] 32 | (Durations/nanos ret) 33 | (or read-default current-duration))))))) 34 | 35 | (defn conf->sec-index 36 | "Creates secondary index for evictions" 37 | [{:memento.core/keys [concurrency]}] 38 | (SecondaryIndex. (or concurrency 4))) 39 | 40 | (defn ^Caffeine conf->builder 41 | "Creates and configures common parameters on the builder." 42 | [{:memento.core/keys [initial-capacity size< ttl fade] 43 | :memento.caffeine/keys [weight< removal-listener kv-weight weak-keys weak-values 44 | soft-values refresh stats ticker expiry]}] 45 | (cond-> (Caffeine/newBuilder) 46 | removal-listener (.removalListener (CaffeineCache_/listener removal-listener)) 47 | initial-capacity (.initialCapacity initial-capacity) 48 | weight< (.maximumWeight weight<) 49 | size< (.maximumSize size<) 50 | kv-weight (.weigher 51 | (reify Weigher (weigh [_this k v] 52 | (kv-weight (.getId ^CacheKey k) 53 | (.getArgs ^CacheKey k) 54 | (b/unwrap-meta v))))) 55 | ;; these don't make sense as the caller cannot hold the CacheKey 56 | ;;weak-keys (.weakKeys) 57 | ;; careful around EntryMeta objects 58 | ;; mean that cached values have another wrapper yet again 59 | weak-values (.weakValues) 60 | soft-values (.softValues) 61 | expiry (.expireAfter (create-expiry ttl fade expiry)) 62 | (and (not expiry) ttl) (.expireAfterWrite (Durations/nanos ttl) TimeUnit/NANOSECONDS) 63 | (and (not expiry) fade) (.expireAfterAccess (Durations/nanos fade) TimeUnit/NANOSECONDS) 64 | ;; not currently used because we don't build a loading cache 65 | refresh (.refreshAfterWrite (Durations/nanos refresh) TimeUnit/NANOSECONDS) 66 | ticker (.ticker (proxy [Ticker] [] (read [] (ticker)))) 67 | stats (.recordStats))) 68 | 69 | (defn assoc-imm-val! 70 | "If cached value is a completable future with immediately available value, assoc it to transient." 71 | [transient-m k v xf] 72 | (let [cv (if (instance? SpecialPromise v) 73 | (.getNow ^SpecialPromise v) 74 | v)] 75 | (if (identical? cv b/absent) 76 | transient-m 77 | (assoc! transient-m k (xf cv))))) 78 | 79 | ;;;;;;;;;;;;;; 80 | ; ->key-fn take 2 args f and key 81 | (defrecord CaffeineCache [conf ^CaffeineCache_ caffeine-cache] 82 | ICache 83 | (conf [this] conf) 84 | (cached [this segment args] 85 | (.cached caffeine-cache segment args)) 86 | (ifCached [this segment args] 87 | (.ifCached caffeine-cache segment args)) 88 | (invalidate [this segment] 89 | (.invalidate caffeine-cache ^Segment segment) 90 | this) 91 | (invalidate [this segment args] (.invalidate caffeine-cache ^Segment segment args) 92 | this) 93 | (invalidateAll [this] (.invalidateAll caffeine-cache) this) 94 | (invalidateIds [this ids] 95 | (.invalidateIds caffeine-cache ids) 96 | this) 97 | (addEntries [this segment args-to-vals] 98 | (.addEntries caffeine-cache segment args-to-vals) 99 | this) 100 | (asMap [this] (persistent! 101 | (reduce (fn [m [k v]] (assoc-imm-val! m k v b/unwrap-meta)) 102 | (transient {}) 103 | (.asMap caffeine-cache)))) 104 | (asMap [this segment] 105 | (persistent! 106 | (reduce (fn [m [^CacheKey k v]] 107 | (if (= (.getId segment) (.getId k)) (assoc-imm-val! m (.getArgs k) v b/unwrap-meta) 108 | m)) 109 | (transient {}) 110 | (.asMap caffeine-cache))))) 111 | 112 | (defmethod b/new-cache :memento.core/caffeine [conf] 113 | (->CaffeineCache conf (CaffeineCache_. 114 | (conf->builder conf) 115 | (:memento.core/key-fn conf) 116 | (:memento.core/ret-fn conf) 117 | (:memento.core/ret-ex-fn conf) 118 | (conf->sec-index conf)))) 119 | 120 | (defn stats 121 | "Return caffeine stats for the cache if it is a caffeine Cache. 122 | 123 | Takes a memoized fn or a Cache instance as a parameter. 124 | 125 | Returns com.github.benmanes.caffeine.cache.stats.CacheStats" 126 | [fn-or-cache] 127 | (if (instance? ICache fn-or-cache) 128 | (when (instance? CaffeineCache fn-or-cache) 129 | (.stats ^CaffeineCache_ (:caffeine-cache fn-or-cache))) 130 | (stats (.mountedCache ^IMountPoint fn-or-cache)))) 131 | 132 | (defn to-data [cache] 133 | (when-let [caffeine (:caffeine-cache cache)] 134 | (persistent! 135 | (reduce (fn [m [^CacheKey k v]] (assoc-imm-val! m 136 | [(.getId k) (.getArgs k)] 137 | v 138 | #(if (and (instance? EntryMeta %) (nil? (.getV ^EntryMeta %))) 139 | nil %))) 140 | (transient {}) 141 | (.asMap ^CaffeineCache_ caffeine))))) 142 | 143 | (defn load-data [cache data-map] 144 | (.loadData ^CaffeineCache_ (:caffeine-cache cache) data-map) 145 | cache) 146 | -------------------------------------------------------------------------------- /src/memento/mount.clj: -------------------------------------------------------------------------------- 1 | (ns memento.mount 2 | "Mount points, they serve as glue between a cache that can house entries from 3 | multiple functions and the individual functions." 4 | {:author "Rok Lenarčič"} 5 | (:require [memento.base :as base] 6 | [memento.config :as config]) 7 | (:import (clojure.lang AFn ISeq MultiFn) 8 | (memento.base ICache Segment) 9 | (memento.mount Cached CachedFn CachedMultiFn IMountPoint))) 10 | 11 | (def ^:dynamic *caches* "Contains map of mount point to cache instance" {}) 12 | (def tags "Map tag to mount-point" (atom {})) 13 | 14 | (def configuration-props [config/key-fn config/ret-fn config/seed config/tags 15 | config/evt-fn config/id config/key-fn* config/ret-ex-fn]) 16 | 17 | (defn assoc-cache-tags 18 | "Add Mount Point ref to tag index" 19 | [index cache-tags ref] 20 | (reduce #(update %1 %2 (fnil conj #{}) ref) index cache-tags)) 21 | 22 | (defn dissoc-cache-tags 23 | "Remove Mount Point ref from tag index" 24 | [index ref] 25 | (reduce-kv #(assoc %1 %2 (disj %3 ref)) {} index)) 26 | 27 | (deftype TagsUnloader [cache-mount] 28 | Runnable 29 | (run [this] 30 | (swap! tags dissoc-cache-tags cache-mount) 31 | (alter-var-root #'*caches* dissoc cache-mount) 32 | nil)) 33 | 34 | (defrecord UntaggedMountPoint [^ICache cache ^Segment segment evt-handler] 35 | IMountPoint 36 | (asMap [this] (.asMap cache segment)) 37 | (cached [this args] (.cached cache segment args)) 38 | (ifCached [this args] (.ifCached cache segment args)) 39 | (getTags [this] []) 40 | (handleEvent [this evt] (evt-handler this evt)) 41 | (invalidate [this args] (.invalidate cache segment args)) 42 | (invalidateAll [this] (.invalidate cache segment)) 43 | (mountedCache [this] cache) 44 | (addEntries [this args-to-vals] (.addEntries cache segment args-to-vals)) 45 | (segment [this] segment)) 46 | 47 | (defrecord TaggedMountPoint [tags ^Segment segment evt-handler] 48 | IMountPoint 49 | (asMap [this] (.asMap ^ICache (*caches* this base/no-cache) segment)) 50 | (cached [this args] (.cached ^ICache (*caches* this base/no-cache) segment args)) 51 | (ifCached [this args] (.ifCached ^ICache (*caches* this base/no-cache) segment args)) 52 | (getTags [this] tags) 53 | (handleEvent [this evt] (evt-handler this evt)) 54 | (invalidate [this args] (.invalidate ^ICache (*caches* this base/no-cache) segment args)) 55 | (invalidateAll [this] (.invalidate ^ICache (*caches* this base/no-cache) segment)) 56 | (mountedCache [this] (*caches* this base/no-cache)) 57 | (addEntries [this args-to-vals] 58 | (.addEntries ^ICache (*caches* this base/no-cache) segment args-to-vals)) 59 | (segment [this] segment)) 60 | 61 | (defn mounted-cache [^IMountPoint mp] (.mountedCache mp)) 62 | 63 | (defn reify-mount-conf 64 | "Transform user given mount-conf to a canonical form of a map." 65 | [mount-conf] 66 | (if (map? mount-conf) 67 | mount-conf 68 | {config/tags ((if (sequential? mount-conf) vec vector) mount-conf)})) 69 | 70 | (defn wrap-fn 71 | [f ret-fn ret-ex-fn] 72 | (cond 73 | (and ret-fn ret-ex-fn) (fn [& args] 74 | (try (ret-fn args (AFn/applyToHelper f args)) 75 | (catch Throwable t (throw (ret-ex-fn args t))))) 76 | ret-fn (fn [& args] (ret-fn args (AFn/applyToHelper f args))) 77 | ret-ex-fn (fn [& args] 78 | (try (AFn/applyToHelper f args) 79 | (catch Throwable t (throw (ret-ex-fn args t))))) 80 | :else f)) 81 | 82 | (defn create-mount 83 | "Create mount record by specified map conf" 84 | [f cache mount-conf] 85 | (let [key-fn (or (config/key-fn mount-conf) 86 | (when-let [base (config/key-fn* mount-conf)] 87 | (fn [args] (AFn/applyToHelper base (if (instance? ISeq args) args (seq args))))) 88 | identity) 89 | evt-fn (config/evt-fn mount-conf (fn [_ _] nil)) 90 | f* (wrap-fn f (config/ret-fn mount-conf) (config/ret-ex-fn mount-conf)) 91 | segment (Segment. f* key-fn (mount-conf config/id f) mount-conf)] 92 | (if-let [t (config/tags mount-conf)] 93 | (let [wrapped-t (if (sequential? t) t (vector t)) 94 | mp (->TaggedMountPoint wrapped-t segment evt-fn)] 95 | (alter-var-root #'*caches* assoc mp cache) 96 | (swap! tags assoc-cache-tags wrapped-t mp) 97 | mp) 98 | (->UntaggedMountPoint cache segment evt-fn)))) 99 | 100 | (defn bind 101 | "Bind a cache to a fn or var. Internal function." 102 | [fn-or-var mount-conf cache] 103 | (if (var? fn-or-var) 104 | (let [mount-conf (-> mount-conf 105 | reify-mount-conf 106 | (update config/id #(or % (.intern (str fn-or-var)))))] 107 | (alter-var-root fn-or-var bind mount-conf cache)) 108 | (let [mount-conf (reify-mount-conf mount-conf) 109 | constructor (if (instance? MultiFn fn-or-var) 110 | #(CachedMultiFn. (str (config/id mount-conf)) %1 %2 %3 %4) 111 | #(CachedFn. %1 %2 %3 %4)) 112 | stacking (if (instance? Cached fn-or-var) (config/bind-mode mount-conf :new) :none) 113 | ^IMountPoint cache-mount (case stacking 114 | :new (create-mount (.getOriginalFn ^Cached fn-or-var) cache mount-conf) 115 | :keep (.getMp ^Cached fn-or-var) 116 | (:none :stack) (create-mount fn-or-var cache mount-conf)) 117 | reload-guard (when (and config/reload-guards? (config/tags mount-conf) (not= :keep stacking)) 118 | (doto (Object.) 119 | (IMountPoint/register (->TagsUnloader cache-mount)))) 120 | f (case stacking 121 | :keep fn-or-var 122 | (:new :stack) (constructor reload-guard cache-mount (meta fn-or-var) (.getOriginalFn ^Cached fn-or-var)) 123 | :none (constructor reload-guard cache-mount (meta fn-or-var) fn-or-var))] 124 | (.addEntries (.getMp ^Cached f) (config/seed mount-conf {})) 125 | f))) 126 | 127 | (defn mount-point 128 | "Return active mount point from the object's meta." 129 | [obj] 130 | (when (instance? IMountPoint obj) obj)) 131 | 132 | (defn update-existing 133 | "Convenience function. Updates ks's that are present with the provided update fn." 134 | [m ks update-fn] 135 | (reduce #(if-let [kv (find %1 %2)] (assoc %1 %2 (update-fn (val kv))) %1) m ks)) 136 | 137 | (defn alter-caches-mapping 138 | "Internal function. Modifies entire tagged cache map with the provided function. 139 | Applies the function as (fn [*caches* refs & other-update-fn-args])" 140 | [tag update-fn & update-fn-args] 141 | (let [refs (get @tags tag []) 142 | update-fn #(apply update-fn % refs update-fn-args)] 143 | (if (.getThreadBinding #'*caches*) 144 | (var-set #'*caches* (update-fn *caches*)) 145 | (alter-var-root #'*caches* update-fn)))) 146 | -------------------------------------------------------------------------------- /src/memento/config.clj: -------------------------------------------------------------------------------- 1 | (ns memento.config 2 | "Memoization library config. 3 | 4 | Contains global settings that manipulate the cache mechanisms. 5 | See doc strings. 6 | 7 | Contains conf settings as vars. 8 | 9 | Also contains documented definitions of standard options of cache config." 10 | {:author "Rok Lenarčič"} 11 | (:refer-clojure :exclude [type]) 12 | (:import (java.util.concurrent TimeUnit))) 13 | 14 | (def ^:redef enabled? 15 | "If false, then all cache attach operations create a cache that does no 16 | caching (changing this value doesn't affect caches already created). 17 | 18 | Initially has the value of java property `memento.enabled` (defaulting to true)." 19 | (Boolean/valueOf (System/getProperty "memento.enabled" "true"))) 20 | 21 | (def ^:redef reload-guards? 22 | "If true, then whenever a function cached with tags is garbage collected (e.g. after a namespace reload in REPL), 23 | a cleanup is done of global tags map. Can be turned off if you don't intend to reload namespaces or do other 24 | actions that would GC cached function instances. 25 | 26 | Initially has the value of java property `memento.reloadable` (defaulting to true)." 27 | (Boolean/valueOf (System/getProperty "memento.reloadable" "true"))) 28 | 29 | (def ^:dynamic *default-type* "Default cache type." :memento.core/none) 30 | 31 | (def type 32 | "Cache setting, type of cache or region that will be instantiated, a keyword. 33 | 34 | The library has two built-ins: 35 | - memento.core/none 36 | - memento.core/caffeine 37 | 38 | If not specified the caches created default to *default-type*." 39 | :memento.core/type) 40 | 41 | (def bind-mode 42 | "Function bind setting, defaults to :new. It governs what the bind will do if you try to bind 43 | a cache to a function that is already cached, e.g. what happens when memo is called multiple times 44 | on same Var. Options are: 45 | - :keep, keeps old cache binding 46 | - :new, keeps the new cache binding 47 | - :stack, stacks the caches, so the new binding wraps the older cached function" 48 | :memento.core/bind-mode) 49 | 50 | (def key-fn 51 | "Cache and function bind setting, a function to be used to calculate the cache key (fn [f-args] key). 52 | 53 | Cache key affects what is considered the 'same' argument list for a function and it will affect caching in that manner. 54 | 55 | If both function bind and cache have this setting, then function bind key-fn is applied first. 56 | 57 | It's a function of 1 argument, the seq of function arguments. If not provided it defaults to identity." 58 | :memento.core/key-fn) 59 | 60 | (def key-fn* 61 | "Function bind setting, works same as key-fn but the provided function will receive all 62 | arguments that the original function does. If both key-fn and key-fn* are provided, key-fn is used." 63 | :memento.core/key-fn*) 64 | 65 | (def ret-fn 66 | "Cache and function bind setting, a function that is ran to process the return of the function, before it's memoized, 67 | (fn [fn-args ret-value] transformed-value). 68 | 69 | It can provide some generic transformation facility, but more importantly, it can wrap specific return 70 | values in 'do-not-cache' object, that prevents caching or wrap with tagged IDs." 71 | :memento.core/ret-fn) 72 | 73 | (def evt-fn 74 | "Function bind setting, a function that is invoked when any event is fired at the function. 75 | 76 | (fn [mnt-point event] void) 77 | 78 | Useful generally to push data into the related cache, the mnt-point parameter implement MountPoint protocol 79 | so you can invoke memo-add! and such on it. The event can be any data, it's probably best to come up with 80 | a format that enables the functions that receive the event to be able to tell them apart." 81 | :memento.core/evt-fn) 82 | 83 | (def seed 84 | "Function bind setting, a map of cache keys to values that will be preloaded when cache is bound." 85 | :memento.core/seed) 86 | 87 | (def ^:deprecated guava 88 | "DEPRECATED: Cache setting value, now points to caffeine implementation" 89 | :memento.core/caffeine) 90 | 91 | (def caffeine 92 | "Cache setting value, type name of Caffeine cache implementation" 93 | :memento.core/caffeine) 94 | 95 | (def none 96 | "Cache setting value, type name of noop cache implementation" 97 | :memento.core/none) 98 | 99 | (def ^:deprecated concurrency 100 | "DEPRECATED: it does nothing in Caffeine implementation" 101 | :memento.core/concurrency) 102 | 103 | (def initial-capacity 104 | "Cache setting, supported by: caffeine, an int. 105 | 106 | Sets the minimum total size for the internal hash tables. Providing a large enough estimate 107 | at construction time avoids the need for expensive resizing operations later, 108 | but setting this value unnecessarily high wastes memory." 109 | :memento.core/initial-capacity) 110 | 111 | (def size< 112 | "Cache setting, supported by: caffeine, a long. 113 | 114 | Specifies the maximum number of entries the cache may contain. Some implementations might evict entries 115 | even before the number of entries reaches the limit." 116 | :memento.core/size<) 117 | 118 | (def ttl 119 | "Cache and Function bind setting, a duration. 120 | 121 | Specifies that each entry should be automatically removed from the cache once a duration 122 | has elapsed after the entry's creation, or the most recent replacement of its value via a put. 123 | 124 | Duration is specified by a number (of seconds) or a vector of a number and unit keyword. 125 | So ttl of 10 or [10 :s] is the same. See 'timeunits' var." 126 | :memento.core/ttl) 127 | 128 | (def fade 129 | "Cache and Function bind setting, a duration. 130 | 131 | Specifies that each entry should be automatically removed from the cache once a fixed duration 132 | has elapsed after the entry's creation, the most recent replacement of its value, or its last access. 133 | Access time is reset by all cache read and write operations. 134 | 135 | Duration is specified by a number (of seconds) or a vector of a number and unit keyword. 136 | So fade of 10 or [10 :s] is the same. See 'timeunits' var." 137 | :memento.core/fade) 138 | 139 | (def tags 140 | "Function bind setting. 141 | 142 | List of tags for this memoized bind." 143 | :memento.core/tags) 144 | 145 | (def id 146 | "Function bind setting. 147 | 148 | Id of the function bind. If you're memoizing a Var, this defaults to stringified var name, 149 | otherwise the ID is the function itself. 150 | 151 | This is useful to specify when you're using Cache implementation that stores data outside JVM, 152 | as they often need a name for each function's cache." 153 | :memento.core/id) 154 | 155 | (def timeunits 156 | "Timeunits keywords, corresponds with Durations class." 157 | {:ns TimeUnit/NANOSECONDS 158 | :us TimeUnit/MICROSECONDS 159 | :ms TimeUnit/MILLISECONDS 160 | :s TimeUnit/SECONDS 161 | :m TimeUnit/MINUTES 162 | :h TimeUnit/HOURS 163 | :d TimeUnit/DAYS}) 164 | 165 | (def cache 166 | "The key extracted from object/var meta and used as cache configuration when 167 | 1-arg memo is called or ns-scan based mounting is performed." 168 | :memento.core/cache) 169 | 170 | (def mount 171 | "The key extracted from object/var meta and used as mount configuration when 172 | 1-arg memo is called or ns-scan based mounting is performed." 173 | :memento.core/mount) 174 | 175 | (def ret-ex-fn 176 | "Cache and function bind setting, a function that is ran to process the throwable thrown by the function, 177 | (fn [fn-args throwable] throwable)." 178 | :memento.core/ret-ex-fn) 179 | -------------------------------------------------------------------------------- /doc/performance.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | Performance is not a dedicated goal of this library, but here's some numbers: 4 | 5 |  6 | 7 |  8 | 9 | ```clojure 10 | ; memoize is not thread-safe and doesn't have any features 11 | (def f-memoize (memoize identity)) 12 | ; clojure.core.memoize 13 | (def f-core-memo (ccm/memo identity)) 14 | ; memento 15 | (def f-memento (m/memo identity {::m/type ::m/caffeine})) 16 | ; memento caffeine variable expiry 17 | (def f-memento-var (m/memo identity {::m/type ::m/caffeine ::m/expiry memento.caffeine.config/meta-expiry})) 18 | ; memento light caffeine 19 | (def f-light-memento (m/memo identity {::m/type ::m/light-caffeine})) 20 | ``` 21 | ## Memoize 22 | 23 | #### All hits 24 | ```text 25 | (cc/bench (f-memoize 1)) 26 | Evaluation count : 2911575540 in 60 samples of 48526259 calls. 27 | Execution time mean : 18,520670 ns 28 | Execution time std-deviation : 0,632964 ns 29 | Execution time lower quantile : 18,041806 ns ( 2,5%) 30 | Execution time upper quantile : 20,272312 ns (97,5%) 31 | Overhead used : 1,997090 ns 32 | 33 | Found 2 outliers in 60 samples (3,3333 %) 34 | low-severe 2 (3,3333 %) 35 | Variance from outliers : 20,6200 % Variance is moderately inflated by outliers 36 | 37 | ``` 38 | 39 | #### 1M misses (426ns per miss) 40 | ```text 41 | (cc/bench (let [f-memoize (memoize identity)] 42 | (reduce #(f-memoize %2) (range 1000000)))) 43 | Evaluation count : 180 in 60 samples of 3 calls. 44 | Execution time mean : 426,691729 ms 45 | Execution time std-deviation : 31,649211 ms 46 | Execution time lower quantile : 407,433346 ms ( 2,5%) 47 | Execution time upper quantile : 500,285216 ms (97,5%) 48 | Overhead used : 1,997090 ns 49 | 50 | Found 9 outliers in 60 samples (15,0000 %) 51 | low-severe 5 (8,3333 %) 52 | low-mild 4 (6,6667 %) 53 | Variance from outliers : 55,1467 % Variance is severely inflated by outliers 54 | ``` 55 | 56 | ## Clojure Core Memoize 57 | 58 | #### All hits 59 | 60 | ```text 61 | (cc/bench (f-core-memo 1)) 62 | Evaluation count : 329229720 in 60 samples of 5487162 calls. 63 | Execution time mean : 180,803852 ns 64 | Execution time std-deviation : 3,880666 ns 65 | Execution time lower quantile : 177,830691 ns ( 2,5%) 66 | Execution time upper quantile : 189,061520 ns (97,5%) 67 | Overhead used : 1,997090 ns 68 | 69 | Found 6 outliers in 60 samples (10,0000 %) 70 | low-severe 3 (5,0000 %) 71 | low-mild 3 (5,0000 %) 72 | Variance from outliers : 9,4347 % Variance is slightly inflated by outliers 73 | ``` 74 | 75 | #### 1M misses (778 ns per miss) 76 | 77 | ```text 78 | (cc/bench (let [f-core-memo (ccm/memo identity)] 79 | (reduce #(f-core-memo %2) (range 1000000)))) 80 | Evaluation count : 120 in 60 samples of 2 calls. 81 | Execution time mean : 778,758053 ms 82 | Execution time std-deviation : 58,068726 ms 83 | Execution time lower quantile : 717,950541 ms ( 2,5%) 84 | Execution time upper quantile : 947,641405 ms (97,5%) 85 | Overhead used : 1,997090 ns 86 | 87 | Found 6 outliers in 60 samples (10,0000 %) 88 | low-severe 4 (6,6667 %) 89 | low-mild 2 (3,3333 %) 90 | Variance from outliers : 55,1627 % Variance is severely inflated by outliers 91 | ``` 92 | 93 | #### 1M misses for size 100 LRU cache (1811 ns per miss) 94 | 95 | ```text 96 | (cc/bench (let [f-core-memo (ccm/lru identity :lru/threshold 100)] 97 | (reduce #(f-core-memo %2) (range 1000000)))) 98 | Evaluation count : 60 in 60 samples of 1 calls. 99 | Execution time mean : 1,811235 sec 100 | Execution time std-deviation : 23,960121 ms 101 | Execution time lower quantile : 1,773504 sec ( 2,5%) 102 | Execution time upper quantile : 1,866470 sec (97,5%) 103 | Overhead used : 1,997090 ns 104 | 105 | Found 2 outliers in 60 samples (3,3333 %) 106 | low-severe 2 (3,3333 %) 107 | Variance from outliers : 1,6389 % Variance is slightly inflated by outliers 108 | 109 | ``` 110 | 111 | ## Memento 112 | 113 | #### All hits 114 | 115 | ```text 116 | (cc/bench (f-memento 1)) 117 | 118 | Evaluation count : 854138220 in 60 samples of 14235637 calls. 119 | Execution time mean : 70,745055 ns 120 | Execution time std-deviation : 2,570125 ns 121 | Execution time lower quantile : 68,659819 ns ( 2,5%) 122 | Execution time upper quantile : 74,128774 ns (97,5%) 123 | Overhead used : 1,970580 ns 124 | 125 | Found 2 outliers in 60 samples (3,3333 %) 126 | low-severe 1 (1,6667 %) 127 | low-mild 1 (1,6667 %) 128 | Variance from outliers : 22,2591 % Variance is moderately inflated by outliers 129 | 130 | 131 | ``` 132 | 133 | #### 1M misses (474 ns per miss) 134 | 135 | ```text 136 | (cc/bench (let [f-memento (m/memo identity {::m/type ::m/caffeine})] 137 | (reduce #(f-memento %2) (range 1000000)))) 138 | Evaluation count : 120 in 60 samples of 2 calls. 139 | Execution time mean : 474,650866 ms 140 | Execution time std-deviation : 76,082064 ms 141 | Execution time lower quantile : 365,465019 ms ( 2,5%) 142 | Execution time upper quantile : 641,739223 ms (97,5%) 143 | Overhead used : 1,992837 ns 144 | 145 | ``` 146 | 147 | #### 1M misses for size 100 LRU cache (338 ns per miss) 148 | 149 | ```text 150 | (cc/bench (let [f-memento (m/memo identity {::m/size< 100 ::m/type ::m/caffeine})] 151 | (reduce #(f-memento %2) (range 1000000)))) 152 | Evaluation count : 180 in 60 samples of 3 calls. 153 | Execution time mean : 338,339882 ms 154 | Execution time std-deviation : 15,865012 ms 155 | Execution time lower quantile : 321,764748 ms ( 2,5%) 156 | Execution time upper quantile : 370,249429 ms (97,5%) 157 | Overhead used : 1,970580 ns 158 | 159 | Found 4 outliers in 60 samples (6,6667 %) 160 | low-severe 3 (5,0000 %) 161 | low-mild 1 (1,6667 %) 162 | Variance from outliers : 33,5491 % Variance is moderately inflated by outliers 163 | 164 | 165 | ``` 166 | 167 | ## Memento Variable Expiry 168 | 169 | #### All hits 170 | 171 | ```text 172 | (cc/bench (f-memento-var 1)) 173 | 174 | Evaluation count : 453412980 in 60 samples of 7556883 calls. 175 | Execution time mean : 132,501700 ns 176 | Execution time std-deviation : 2,015071 ns 177 | Execution time lower quantile : 130,326931 ns ( 2,5%) 178 | Execution time upper quantile : 134,890796 ns (97,5%) 179 | Overhead used : 1,978672 ns 180 | 181 | ``` 182 | 183 | #### 1M misses (526 ns per miss) 184 | 185 | ```text 186 | (cc/bench (let [f-memento-var (m/memo identity {::m/type ::m/caffeine ::m/expiry memento.caffeine.config/meta-expiry})] 187 | (reduce #(f-memento-var %2) (range 1000000)))) 188 | Evaluation count : 120 in 60 samples of 2 calls. 189 | Execution time mean : 526,197766 ms 190 | Execution time std-deviation : 59,110910 ms 191 | Execution time lower quantile : 426,811124 ms ( 2,5%) 192 | Execution time upper quantile : 644,451645 ms (97,5%) 193 | Overhead used : 1,978672 ns 194 | 195 | ``` 196 | 197 | #### 1M misses for size 100 LRU cache (387 ns per miss) 198 | 199 | ```text 200 | (cc/bench (let [f-memento-var (m/memo identity {::m/size< 100 ::m/type ::m/caffeine ::m/expiry memento.caffeine.config/meta-expiry})] 201 | (reduce #(f-memento-var %2) (range 1000000)))) 202 | Evaluation count : 180 in 60 samples of 3 calls. 203 | Execution time mean : 423,554590 ms 204 | Execution time std-deviation : 7,825220 ms 205 | Execution time lower quantile : 414,372683 ms ( 2,5%) 206 | Execution time upper quantile : 435,451863 ms (97,5%) 207 | Overhead used : 1,978672 ns 208 | 209 | ``` -------------------------------------------------------------------------------- /java/memento/caffeine/CaffeineCache_.java: -------------------------------------------------------------------------------- 1 | package memento.caffeine; 2 | 3 | import clojure.lang.*; 4 | import com.github.benmanes.caffeine.cache.Cache; 5 | import com.github.benmanes.caffeine.cache.Caffeine; 6 | import com.github.benmanes.caffeine.cache.RemovalListener; 7 | import com.github.benmanes.caffeine.cache.stats.CacheStats; 8 | import memento.base.CacheKey; 9 | import memento.base.EntryMeta; 10 | import memento.base.LockoutMap; 11 | import memento.base.Segment; 12 | 13 | import java.util.*; 14 | import java.util.concurrent.ConcurrentHashMap; 15 | import java.util.concurrent.ConcurrentMap; 16 | import java.util.function.BiFunction; 17 | 18 | public class CaffeineCache_ { 19 | 20 | private final BiFunction keyFn; 21 | 22 | private final SecondaryIndex secIndex; 23 | private final IFn retFn; 24 | 25 | private final IFn retExFn; 26 | 27 | private final Cache delegate; 28 | 29 | private final Set loads = ConcurrentHashMap.newKeySet(); 30 | 31 | public CaffeineCache_(Caffeine builder, final IFn keyFn, final IFn retFn, final IFn retExFn, SecondaryIndex secIndex) { 32 | this.keyFn = keyFn == null ? 33 | (segment, args) -> new CacheKey(segment.getId(), segment.getKeyFn().invoke(args)) : 34 | (segment, args) -> new CacheKey(segment.getId(), keyFn.invoke(segment.getKeyFn().invoke(args))); 35 | this.retFn = retFn; 36 | this.delegate = builder.build(); 37 | this.secIndex = secIndex; 38 | this.retExFn = retExFn; 39 | } 40 | 41 | private void initLoad(SpecialPromise promise) { 42 | promise.init(); 43 | loads.add(promise); 44 | } 45 | 46 | public Object cached(Segment segment, ISeq args) throws Throwable { 47 | CacheKey key = keyFn.apply(segment, args); 48 | do { 49 | SpecialPromise p = new SpecialPromise(); 50 | // check for ongoing load 51 | Object cached = delegate.asMap().putIfAbsent(key, p); 52 | if (cached == null) { 53 | try { 54 | initLoad(p); 55 | // calculate value 56 | Object result = AFn.applyToHelper(segment.getF(), args); 57 | if (retFn != null) { 58 | result = retFn.invoke(args, result); 59 | } 60 | if (!p.deliver(result)) { 61 | // The SpecialPromise was invalidated, restart the process 62 | delegate.asMap().remove(key, p); 63 | continue; 64 | } 65 | // if valid add to secondary index 66 | secIndex.add(key, result); 67 | if (result instanceof EntryMeta && ((EntryMeta) result).isNoCache()) { 68 | delegate.asMap().remove(key, p); 69 | } else { 70 | delegate.asMap().replace(key, p, result == null ? EntryMeta.NIL : result); 71 | } 72 | return EntryMeta.unwrap(result); 73 | } catch (Throwable t) { 74 | delegate.asMap().remove(key, p); 75 | if (!p.isInvalid()) { 76 | p.deliverException(retExFn == null ? t : (Throwable) retExFn.invoke(args, t)); 77 | throw t; 78 | } 79 | } finally { 80 | p.releaseResult(); 81 | loads.remove(p); 82 | } 83 | } else { 84 | // join into ongoing load 85 | if (cached instanceof SpecialPromise) { 86 | SpecialPromise sp = (SpecialPromise) cached; 87 | Object ret = sp.await(key); 88 | if (ret != EntryMeta.absent && !LockoutMap.awaitLockout(ret)) { 89 | // if not invalidated, return the value 90 | return EntryMeta.unwrap(ret); 91 | } 92 | } else { 93 | if (!LockoutMap.awaitLockout(cached)) { 94 | // if not invalidated, return the value 95 | return EntryMeta.unwrap(cached); 96 | } 97 | } 98 | // else try to initiate load again 99 | } 100 | } while (true); 101 | } 102 | 103 | public Object ifCached(Segment segment, ISeq args) throws Throwable { 104 | CacheKey key = keyFn.apply(segment, args); 105 | Object v = delegate.getIfPresent(key); 106 | Object absent = EntryMeta.absent; 107 | if (v == null) { 108 | return absent; 109 | } else if (v instanceof SpecialPromise) { 110 | SpecialPromise p = (SpecialPromise) v; 111 | Object ret = p.getNow(); 112 | if (ret == absent || LockoutMap.awaitLockout(ret)) { 113 | return absent; 114 | } else { 115 | return EntryMeta.unwrap(ret); 116 | } 117 | } else { 118 | return LockoutMap.awaitLockout(v) ? absent : EntryMeta.unwrap(v); 119 | } 120 | } 121 | 122 | public void invalidate(Segment segment) { 123 | final Iterator> iter = delegate.asMap().entrySet().iterator(); 124 | while (iter.hasNext()) { 125 | Map.Entry it = iter.next(); 126 | if (it.getKey().getId().equals(segment.getId())) { 127 | Object v = it.getValue(); 128 | if (v instanceof SpecialPromise) { 129 | ((SpecialPromise) v).invalidate(); 130 | } 131 | iter.remove(); 132 | } 133 | } 134 | } 135 | 136 | public void invalidate(Segment segment, ISeq args) { 137 | Object v = delegate.asMap().remove(keyFn.apply(segment, args)); 138 | if (v instanceof SpecialPromise) { 139 | ((SpecialPromise) v).invalidate(); 140 | } 141 | } 142 | 143 | public void invalidateAll() { 144 | delegate.invalidateAll(); 145 | } 146 | 147 | public void invalidateIds(Iterable ids) { 148 | HashSet keys = new HashSet<>(); 149 | for (Object id : ids) { 150 | secIndex.drainKeys(id, keys::add); 151 | } 152 | ConcurrentMap map = delegate.asMap(); 153 | for (CacheKey k : keys) { 154 | Object removed = map.remove(k); 155 | if (removed instanceof SpecialPromise) { 156 | ((SpecialPromise) removed).invalidate(); 157 | } 158 | } 159 | loads.forEach(row -> row.addInvalidIds(ids)); 160 | } 161 | 162 | public void addEntries(Segment segment, IPersistentMap argsToVals) { 163 | for (Object o : argsToVals) { 164 | MapEntry entry = (MapEntry) o; 165 | CacheKey key = keyFn.apply(segment, RT.seq(entry.getKey())); 166 | Object val = entry.getValue(); 167 | secIndex.add(key, val); 168 | delegate.put(key, val == null ? EntryMeta.NIL : val); 169 | } 170 | } 171 | 172 | public ConcurrentMap asMap() { 173 | return delegate.asMap(); 174 | } 175 | 176 | public CacheStats stats() { 177 | return delegate.stats(); 178 | } 179 | 180 | public void loadData(Map map) { 181 | map.forEach((Object k, Object v) -> { 182 | List list = (List) k; 183 | CacheKey key = new CacheKey(list.get(0), list.get(1)); 184 | secIndex.add(key, v); 185 | delegate.put(key, v == null ? EntryMeta.NIL : v); 186 | }); 187 | } 188 | 189 | public static RemovalListener listener(IFn removalListener) { 190 | return (k, v, removalCause) -> { 191 | if (!(v instanceof SpecialPromise)) { 192 | removalListener.invoke(k.getId(), k.getArgs(), v instanceof EntryMeta ? ((EntryMeta) v).getV() : v, removalCause); 193 | } 194 | }; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /java/memento/multi/TieredCache.java: -------------------------------------------------------------------------------- 1 | package memento.multi; 2 | 3 | import clojure.lang.ArraySeq; 4 | import clojure.lang.IFn; 5 | import clojure.lang.IPersistentMap; 6 | import clojure.lang.ISeq; 7 | import memento.base.ICache; 8 | import memento.base.Segment; 9 | 10 | public class TieredCache extends MultiCache { 11 | public TieredCache(ICache cache, ICache upstream, IPersistentMap conf, Object absent) { 12 | super(cache, upstream, conf, absent); 13 | } 14 | 15 | @Override 16 | public Object cached(Segment segment, ISeq args) { 17 | return cache.cached(segment.withFn(new AskUpstream(segment)), args); 18 | } 19 | 20 | private class AskUpstream implements IFn { 21 | 22 | private final Segment segment; 23 | 24 | public AskUpstream(Segment segment) { 25 | this.segment = segment; 26 | } 27 | 28 | @Override 29 | public Object call() { 30 | return upstream.cached(segment, ArraySeq.create()); 31 | } 32 | 33 | @Override 34 | public void run() { 35 | upstream.cached(segment, ArraySeq.create()); 36 | } 37 | 38 | @Override 39 | public Object invoke() { 40 | return upstream.cached(segment, ArraySeq.create()); 41 | } 42 | 43 | @Override 44 | public Object invoke(Object arg1) { 45 | return upstream.cached(segment, ArraySeq.create(arg1)); 46 | } 47 | 48 | @Override 49 | public Object invoke(Object arg1, Object arg2) { 50 | return upstream.cached(segment, ArraySeq.create(arg1, arg2)); 51 | } 52 | 53 | @Override 54 | public Object invoke(Object arg1, Object arg2, Object arg3) { 55 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3)); 56 | } 57 | 58 | @Override 59 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 60 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4)); 61 | } 62 | 63 | @Override 64 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 65 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5)); 66 | } 67 | 68 | @Override 69 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 70 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6)); 71 | } 72 | 73 | @Override 74 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 75 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7)); 76 | } 77 | 78 | @Override 79 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 80 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)); 81 | } 82 | 83 | @Override 84 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 85 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)); 86 | } 87 | 88 | @Override 89 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 90 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); 91 | } 92 | 93 | @Override 94 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 95 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11)); 96 | } 97 | 98 | @Override 99 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 100 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12)); 101 | } 102 | 103 | @Override 104 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 105 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13)); 106 | } 107 | 108 | @Override 109 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 110 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14)); 111 | } 112 | 113 | @Override 114 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 115 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15)); 116 | } 117 | 118 | @Override 119 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 120 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16)); 121 | } 122 | 123 | @Override 124 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 125 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17)); 126 | } 127 | 128 | @Override 129 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 130 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18)); 131 | } 132 | 133 | @Override 134 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 135 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19)); 136 | } 137 | 138 | @Override 139 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 140 | return upstream.cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20)); 141 | } 142 | 143 | @Override 144 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 145 | Object[] allArgs = new Object[20 + args.length]; 146 | System.arraycopy(args, 0, allArgs, 20, args.length); 147 | allArgs[0] = arg1; 148 | allArgs[1] = arg2; 149 | allArgs[2] = arg3; 150 | allArgs[3] = arg4; 151 | allArgs[4] = arg5; 152 | allArgs[5] = arg6; 153 | allArgs[6] = arg7; 154 | allArgs[7] = arg8; 155 | allArgs[8] = arg9; 156 | allArgs[9] = arg10; 157 | allArgs[10] = arg11; 158 | allArgs[11] = arg12; 159 | allArgs[12] = arg13; 160 | allArgs[13] = arg14; 161 | allArgs[14] = arg15; 162 | allArgs[15] = arg16; 163 | allArgs[16] = arg17; 164 | allArgs[17] = arg18; 165 | allArgs[18] = arg19; 166 | allArgs[19] = arg20; 167 | return upstream.cached(segment, ArraySeq.create(allArgs)); 168 | } 169 | 170 | @Override 171 | public Object applyTo(ISeq arglist) { 172 | return upstream.cached(segment, arglist); 173 | } 174 | 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /java/memento/mount/CachedFn.java: -------------------------------------------------------------------------------- 1 | package memento.mount; 2 | 3 | import clojure.lang.*; 4 | import memento.base.ICache; 5 | import memento.base.Segment; 6 | 7 | public class CachedFn extends AFunction implements IMountPoint, Cached { 8 | private final Object reloadGuard; 9 | private final IFn originalFn; 10 | private final Segment segment; 11 | 12 | @Override 13 | public IPersistentMap asMap() { 14 | return mp.asMap(); 15 | } 16 | 17 | @Override 18 | public Object cached(ISeq args) { 19 | return mp.cached(args); 20 | } 21 | 22 | @Override 23 | public Object ifCached(ISeq args) { 24 | return mp.ifCached(args); 25 | } 26 | 27 | @Override 28 | public Object getTags() { 29 | return mp.getTags(); 30 | } 31 | 32 | @Override 33 | public Object handleEvent(Object event) { 34 | return mp.handleEvent(event); 35 | } 36 | 37 | @Override 38 | public ICache invalidate(ISeq args) { 39 | return mp.invalidate(args); 40 | } 41 | 42 | @Override 43 | public ICache invalidateAll() { 44 | return mp.invalidateAll(); 45 | } 46 | 47 | @Override 48 | public ICache mountedCache() { 49 | return mp.mountedCache(); 50 | } 51 | 52 | @Override 53 | public Segment segment() { 54 | return mp.segment(); 55 | } 56 | 57 | @Override 58 | public ICache addEntries(IPersistentMap argsToVals) { 59 | return mp.addEntries(argsToVals); 60 | } 61 | 62 | private final IMountPoint mp; 63 | private final IPersistentMap meta; 64 | 65 | public CachedFn(Object reloadGuard, IMountPoint mp, IPersistentMap meta, IFn originalFn) { 66 | this.reloadGuard = reloadGuard; 67 | this.mp = mp; 68 | this.meta = meta; 69 | this.originalFn = originalFn; 70 | this.segment = mp.segment(); 71 | } 72 | 73 | @Override 74 | public IPersistentMap meta() { 75 | return meta; 76 | } 77 | 78 | @Override 79 | public IObj withMeta(IPersistentMap meta) { 80 | return new CachedFn(reloadGuard, mp, meta, originalFn); 81 | } 82 | 83 | @Override 84 | public Object call() { 85 | return mp.mountedCache().cached(segment, ArraySeq.create()); 86 | } 87 | 88 | @Override 89 | public void run() { 90 | mp.mountedCache().cached(segment, ArraySeq.create()); 91 | } 92 | 93 | @Override 94 | public Object invoke() { 95 | return mp.mountedCache().cached(segment, ArraySeq.create()); 96 | } 97 | 98 | @Override 99 | public Object invoke(Object arg1) { 100 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1)); 101 | } 102 | 103 | @Override 104 | public Object invoke(Object arg1, Object arg2) { 105 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2)); 106 | } 107 | 108 | @Override 109 | public Object invoke(Object arg1, Object arg2, Object arg3) { 110 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3)); 111 | } 112 | 113 | @Override 114 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 115 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4)); 116 | } 117 | 118 | @Override 119 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 120 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5)); 121 | } 122 | 123 | @Override 124 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 125 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6)); 126 | } 127 | 128 | @Override 129 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 130 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7)); 131 | } 132 | 133 | @Override 134 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 135 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)); 136 | } 137 | 138 | @Override 139 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 140 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)); 141 | } 142 | 143 | @Override 144 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 145 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); 146 | } 147 | 148 | @Override 149 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 150 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11)); 151 | } 152 | 153 | @Override 154 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 155 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12)); 156 | } 157 | 158 | @Override 159 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 160 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13)); 161 | } 162 | 163 | @Override 164 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 165 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14)); 166 | } 167 | 168 | @Override 169 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 170 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15)); 171 | } 172 | 173 | @Override 174 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 175 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16)); 176 | } 177 | 178 | @Override 179 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 180 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17)); 181 | } 182 | 183 | @Override 184 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 185 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18)); 186 | } 187 | 188 | @Override 189 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 190 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19)); 191 | } 192 | 193 | @Override 194 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 195 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20)); 196 | } 197 | 198 | @Override 199 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 200 | Object[] allArgs = new Object[20 + args.length]; 201 | System.arraycopy(args, 0, allArgs, 20, args.length); 202 | allArgs[0] = arg1; 203 | allArgs[1] = arg2; 204 | allArgs[2] = arg3; 205 | allArgs[3] = arg4; 206 | allArgs[4] = arg5; 207 | allArgs[5] = arg6; 208 | allArgs[6] = arg7; 209 | allArgs[7] = arg8; 210 | allArgs[8] = arg9; 211 | allArgs[9] = arg10; 212 | allArgs[10] = arg11; 213 | allArgs[11] = arg12; 214 | allArgs[12] = arg13; 215 | allArgs[13] = arg14; 216 | allArgs[14] = arg15; 217 | allArgs[15] = arg16; 218 | allArgs[16] = arg17; 219 | allArgs[17] = arg18; 220 | allArgs[18] = arg19; 221 | allArgs[19] = arg20; 222 | return mp.mountedCache().cached(segment, ArraySeq.create(allArgs)); 223 | } 224 | 225 | @Override 226 | public Object applyTo(ISeq arglist) { 227 | return mp.mountedCache().cached(segment, arglist); 228 | } 229 | 230 | public IMountPoint getMp() { 231 | return mp; 232 | } 233 | 234 | public IFn getOriginalFn() { 235 | return originalFn; 236 | } 237 | 238 | @Override 239 | public String toString() { 240 | return "CachedFn{" + 241 | "originalFn=" + originalFn + 242 | ", segment=" + segment + 243 | ", mp=" + mp + 244 | ", meta=" + meta + 245 | '}'; 246 | } 247 | 248 | public Segment getSegment() { 249 | return segment; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/memento/core.clj: -------------------------------------------------------------------------------- 1 | (ns memento.core 2 | "Memoization library." 3 | {:author "Rok Lenarčič"} 4 | (:require [memento.base :as base] 5 | [memento.caffeine] 6 | [memento.multi :as multi] 7 | [memento.mount :as mount]) 8 | (:import (java.util IdentityHashMap) 9 | (java.util.function BiFunction) 10 | (memento.base EntryMeta ICache LockoutTag) 11 | (memento.mount Cached IMountPoint))) 12 | 13 | (defn do-not-cache 14 | "Wrap a function result value in a wrapper that tells the Cache not to 15 | cache this particular value." 16 | [v] 17 | (if (instance? EntryMeta v) 18 | (do (.setNoCache ^EntryMeta v true) v) 19 | (EntryMeta. v true #{}))) 20 | 21 | (defn with-tag-id 22 | "Wrap a function result value in a wrapper that has the given additional 23 | tag + ID information. You can add multiple IDs for same tag. 24 | 25 | This information is later used by memo-clear-tag!." 26 | [v tag id] 27 | (if (instance? EntryMeta v) 28 | (do (.setTagIdents ^EntryMeta v (conj (.getTagIdents ^EntryMeta v) [tag id])) v) 29 | (EntryMeta. v false #{[tag id]}))) 30 | 31 | (defn create 32 | "Create a cache. 33 | 34 | A conf is a map of cache settings, see memento.config namespace for names of settings." 35 | [conf] 36 | (base/base-create-cache conf)) 37 | 38 | (defn bind 39 | "Bind the cache to a function or a var. If a var is specified, then var root 40 | binding is modified. 41 | 42 | The mount-conf is a configuration options for mount point. 43 | 44 | It can be a map with options, a vector of tags, or one tag. 45 | 46 | Supported options are: 47 | - memento.core/key-fn 48 | - memento.core/key-fn* 49 | - memento.core/ret-fn 50 | - memento.core/tags 51 | - memento.core/seed" 52 | [fn-or-var mount-conf cache] 53 | (when-not (instance? ICache cache) 54 | (throw (IllegalArgumentException. "Argument should satisfy memento.base/Cache"))) 55 | (mount/bind fn-or-var mount-conf cache)) 56 | 57 | (defn memo 58 | "Combines cache create and bind operations from this namespace. 59 | 60 | If conf is provided, it is used as mount-conf in bind operation, but with any extra map keys 61 | going into cache create configuration. 62 | 63 | If no configuration is provided, meta of the fn or var is examined. 64 | 65 | The value of :memento.core/cache meta key is used as conf parameter 66 | in memento.core/memo. If :memento.core/mount key is also present, then 67 | they are used as cache and conf parameters respectively." 68 | ([fn-or-var] 69 | (let [{::keys [mount cache]} (meta fn-or-var)] 70 | (if mount (memo fn-or-var mount cache) 71 | (memo fn-or-var cache)))) 72 | ([fn-or-var conf] 73 | (if (map? conf) 74 | (memo fn-or-var 75 | (select-keys conf mount/configuration-props) 76 | (apply dissoc conf mount/configuration-props)) 77 | (memo fn-or-var conf {}))) 78 | ([fn-or-var mount-conf cache-conf] 79 | (->> cache-conf 80 | create 81 | (bind fn-or-var mount-conf)))) 82 | 83 | (defmacro defmemo 84 | "Like defn, but immediately wraps var in a memo call. It expects caching configuration 85 | to be in meta under memento.core/cache key, as expected by memo." 86 | {:arglists '([name doc-string? attr-map? [params*] prepost-map? body] 87 | [name doc-string? attr-map? ([params*] prepost-map? body)+ attr-map?])} 88 | [& body] 89 | `(memo (defn ~@body))) 90 | 91 | (defn active-cache 92 | "Return Cache instance from the function, if present." 93 | [f] (some-> (mount/mount-point f) mount/mounted-cache)) 94 | 95 | (defn memoized? 96 | "Returns true if function is memoized." 97 | [f] (instance? Cached f)) 98 | 99 | (defn memo-unwrap 100 | "Takes a function and returns an uncached function." 101 | [f] (if (instance? Cached f) (.getOriginalFn ^Cached f) f)) 102 | 103 | (defn memo-clear-cache! 104 | "Invalidate all entries in Cache. Returns cache." 105 | [cache] 106 | (when-not (instance? ICache cache) 107 | (throw (IllegalArgumentException. "Argument should satisfy memento.base/Cache"))) 108 | (base/invalidate-all cache)) 109 | 110 | (defn none-cache? 111 | "Returns true if this cache is one that does no caching." 112 | [cache] 113 | (= cache base/no-cache)) 114 | 115 | (defn memo-clear! 116 | "Invalidate one entry (f with arglist) on memoized function f, 117 | or invalidate all entries for memoized function. Returns f." 118 | ([f] 119 | (when-let [^IMountPoint mp (mount/mount-point f)] (.invalidateAll mp)) 120 | f) 121 | ([f & fargs] 122 | (when-let [^IMountPoint mp (mount/mount-point f)] (.invalidate mp fargs)) 123 | f)) 124 | 125 | (defn memo-add! 126 | "Add map's entries to the cache. The keys are argument-lists. 127 | 128 | Returns f." 129 | [f m] 130 | (when-let [^IMountPoint mp (mount/mount-point f)] (.addEntries mp m)) 131 | f) 132 | 133 | (defn as-map 134 | "Return a map representation of the memoized entries on this function." 135 | [f] 136 | (when-let [^IMountPoint mp (mount/mount-point f)] (.asMap mp))) 137 | 138 | (defn tags 139 | "Return tags of the memoized function." 140 | [f] 141 | (when-let [^IMountPoint mp (mount/mount-point f)] (.getTags mp))) 142 | 143 | (defn mounts-by-tag 144 | "Returns a sequence of MountPoint instances used by memoized functions which are tagged by this tag." 145 | [tag] 146 | (get @mount/tags tag [])) 147 | 148 | (defn caches-by-tag 149 | "Returns a collection of distinct caches that are mounted with a tag" 150 | [tag] 151 | (let [m (IdentityHashMap.)] 152 | (run! #(.put m (mount/mounted-cache %) nil) (mounts-by-tag tag)) 153 | (.keySet m))) 154 | 155 | (defn fire-event! 156 | "Fire an event payload to the single cached function or all tagged functions, if tag 157 | is provided." 158 | [f-or-tag evt] 159 | (if (instance? IMountPoint f-or-tag) 160 | (.handleEvent ^IMountPoint f-or-tag evt) 161 | (->> (mounts-by-tag f-or-tag) 162 | (eduction (map #(.handleEvent ^IMountPoint % evt))) 163 | dorun))) 164 | 165 | (defn memo-clear-tags! 166 | "Invalidate all entries that have the specified tag + id metadata. ID can be anything. 167 | 168 | Expects a collection of [tag id] pairs." 169 | [& tag+ids] 170 | (let [cache->ids (IdentityHashMap.) 171 | _ (doseq [[tag tag+ids] (group-by first tag+ids) 172 | cache (caches-by-tag tag)] 173 | (.compute 174 | cache->ids 175 | cache 176 | (reify BiFunction 177 | (apply [this k v] (into (or v []) tag+ids))))) 178 | tag (LockoutTag.)] 179 | (try 180 | (.startLockout base/lockout-map tag+ids tag) 181 | (run! (fn [e] (base/invalidate-ids (key e) (val e))) cache->ids) 182 | (finally 183 | (.endLockout base/lockout-map tag+ids tag))))) 184 | 185 | (defn memo-clear-tag! 186 | "Invalidate all entries that have the specified tag + id metadata. ID can be anything." 187 | [tag id] 188 | (memo-clear-tags! [tag id])) 189 | 190 | (defn update-tag-caches! 191 | "For each memoized function with the specified tag, set the Cache used by the fn to (cache-fn current-cache). 192 | 193 | Cache update function is ran on each 194 | memoized function (mount point), so if one cache is backing multiple functions, the cache update function is called 195 | multiple times on it. If you want to run cache-fn one each Cache instance only once, I recommend wrapping it 196 | in clojure.core/memoize. 197 | 198 | If caches are thread-bound to a different value with with-caches, then those 199 | bindings are modified instead of root bindings." 200 | [tag cache-fn] 201 | (mount/alter-caches-mapping tag mount/update-existing cache-fn)) 202 | 203 | (defmacro with-caches 204 | "Within the block, each memoized function with the specified tag has its cache update by cache-fn. 205 | 206 | The values are bound within the block as a thread local binding. Cache update function is ran on each 207 | memoized function (mount point), so if one cache is backing multiple functions, the cache update function is called 208 | multiple timed on it. If you want to run cache-fn one each Cache instance only once, I recommend wrapping it 209 | in clojure.core/memoize." 210 | [tag cache-fn & body] 211 | `(binding [mount/*caches* (mount/update-existing mount/*caches* (get @mount/tags ~tag []) ~cache-fn)] 212 | ~@body)) 213 | 214 | (defn evt-cache-add 215 | "Convenience function. It creates or wraps event handler fn, 216 | with an implementation which expects an event to be a vector of 217 | [event-type payload], it checks for matching event type and inserts 218 | the result of (->entries payload) into the cache." 219 | ([evt-type ->entries] (evt-cache-add (constantly nil) evt-type ->entries)) 220 | ([evt-fn evt-type ->entries] 221 | (fn [mountp evt] 222 | (when (and (vector? evt) 223 | (= (count evt) 2) 224 | (= evt-type (first evt))) 225 | (memo-add! mountp (->entries (second evt)))) 226 | (evt-fn mountp evt)))) 227 | 228 | (defn tiered 229 | "Creates a configuration for a tiered cache. Both parameters are either a conf map or a cache. 230 | 231 | Entry is fetched from cache, delegating to upstream is not found. After the operation 232 | the entry is in both caches. 233 | 234 | Useful when upstream is a big cache that outside the JVM, but it's not that inexpensive, so you 235 | want a local smaller cache in front of it. 236 | 237 | Invalidation operations also affect upstream. Other operations only affect local cache." 238 | [cache upstream] 239 | {::type ::tiered 240 | ::multi/cache cache 241 | ::multi/upstream upstream}) 242 | 243 | (defn consulting 244 | "Creates a configuration for a consulting tiered cache. Both parameters are either a conf map or a cache. 245 | 246 | Entry is fetched from cache, if not found, the upstream is asked for entry if present (but not to make one 247 | in the upstream). 248 | 249 | After the operation, the entry is in local cache, upstream is unchanged. 250 | 251 | Useful when you want to consult a long term upstream cache for existing entries, but you don't want any 252 | entries being created for the short term cache to be pushed upstream. 253 | 254 | Invalidation operations also affect upstream. Other operations only affect local cache." 255 | [cache upstream] 256 | {::type ::consulting 257 | ::multi/cache cache 258 | ::multi/upstream upstream}) 259 | 260 | (defn daisy 261 | "Creates a configuration for a daisy chained cache. Cache parameter is a conf map or a cache. 262 | 263 | Entry is returned from cache IF PRESENT, otherwise upstream is hit. The returned value 264 | is NOT added to cache. 265 | 266 | After the operation the entry is either in local or upstream cache. 267 | 268 | Useful when you don't want entries from upstream accumulating in local 269 | cache, and you're feeding the local cache via some other means: 270 | - a preloaded fixed cache 271 | - manually adding entries 272 | 273 | Invalidation operations also affect upstream. Other operations only affect local cache." 274 | [cache upstream] 275 | {::type ::daisy 276 | ::multi/cache cache 277 | ::multi/upstream upstream}) 278 | 279 | (defmacro if-cached 280 | "Like if-let, but then clause is executed if the call in the binding is cached, with the binding symbol 281 | being bound to the cached value. 282 | 283 | This assumes that the top form in bindings is a call of cached function, generating an error otherwise. 284 | 285 | e.g. (if-cached [my-val (my-cached-fn arg1)] ...)" 286 | ([bindings then] 287 | `(if-cached ~bindings ~then nil)) 288 | ([bindings then else] 289 | (assert (vector? bindings)) 290 | (assert (= 2 (count bindings))) 291 | (let [form (bindings 0) 292 | cache-call (bindings 1) 293 | _ (assert (list? cache-call)) 294 | f (first cache-call) 295 | _ (assert (symbol? f))] 296 | `(if-let [mnt# (mount/mount-point ~(first cache-call))] 297 | (let [mnt# (if (instance? Cached mnt#) (.getMp mnt#) mnt#) 298 | cached# (.ifCached mnt# '~(next cache-call))] 299 | (if (= cached# base/absent) 300 | ~else 301 | (let [~form cached#] ~then))) 302 | (throw (ex-info (str "Function " ~(str f) " is not a cached function") 303 | {:form '~cache-call})))))) 304 | -------------------------------------------------------------------------------- /java/memento/multi/ConsultingCache.java: -------------------------------------------------------------------------------- 1 | package memento.multi; 2 | 3 | import clojure.lang.*; 4 | import memento.base.ICache; 5 | import memento.base.Segment; 6 | 7 | public class ConsultingCache extends MultiCache { 8 | public ConsultingCache(ICache cache, ICache upstream, IPersistentMap conf, Object absent) { 9 | super(cache, upstream, conf, absent); 10 | } 11 | 12 | @Override 13 | public Object cached(Segment segment, ISeq args) { 14 | return cache.cached(segment.withFn(new UpstreamOrCalc(segment)), args); 15 | } 16 | 17 | private class UpstreamOrCalc implements IFn { 18 | 19 | private Segment segment; 20 | 21 | public UpstreamOrCalc(Segment segment) { 22 | this.segment = segment; 23 | } 24 | 25 | @Override 26 | public Object call() { 27 | ISeq s = ArraySeq.create(); 28 | Object up = upstream.ifCached(segment, s); 29 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 30 | } 31 | 32 | @Override 33 | public void run() { 34 | ISeq s = ArraySeq.create(); 35 | Object up = upstream.ifCached(segment, s); 36 | if (up == absent) { 37 | AFn.applyToHelper(segment.getF(), s); 38 | } 39 | } 40 | 41 | @Override 42 | public Object invoke() { 43 | ISeq s = ArraySeq.create(); 44 | Object up = upstream.ifCached(segment, s); 45 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 46 | } 47 | 48 | @Override 49 | public Object invoke(Object arg1) { 50 | ISeq s = ArraySeq.create(arg1); 51 | Object up = upstream.ifCached(segment, s); 52 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 53 | } 54 | 55 | @Override 56 | public Object invoke(Object arg1, Object arg2) { 57 | ISeq s = ArraySeq.create(arg1, arg2); 58 | Object up = upstream.ifCached(segment, s); 59 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 60 | } 61 | 62 | @Override 63 | public Object invoke(Object arg1, Object arg2, Object arg3) { 64 | ISeq s = ArraySeq.create(arg1, arg2, arg3); 65 | Object up = upstream.ifCached(segment, s); 66 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 67 | } 68 | 69 | @Override 70 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 71 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4); 72 | Object up = upstream.ifCached(segment, s); 73 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 74 | } 75 | 76 | @Override 77 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 78 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5); 79 | Object up = upstream.ifCached(segment, s); 80 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 81 | } 82 | 83 | @Override 84 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 85 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6); 86 | Object up = upstream.ifCached(segment, s); 87 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 88 | } 89 | 90 | @Override 91 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 92 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7); 93 | Object up = upstream.ifCached(segment, s); 94 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 95 | } 96 | 97 | @Override 98 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 99 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); 100 | Object up = upstream.ifCached(segment, s); 101 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 102 | } 103 | 104 | @Override 105 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 106 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); 107 | Object up = upstream.ifCached(segment, s); 108 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 109 | } 110 | 111 | @Override 112 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 113 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10); 114 | Object up = upstream.ifCached(segment, s); 115 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 116 | } 117 | 118 | @Override 119 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 120 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11); 121 | Object up = upstream.ifCached(segment, s); 122 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 123 | } 124 | 125 | @Override 126 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 127 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12); 128 | Object up = upstream.ifCached(segment, s); 129 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 130 | } 131 | 132 | @Override 133 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 134 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13); 135 | Object up = upstream.ifCached(segment, s); 136 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 137 | } 138 | 139 | @Override 140 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 141 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14); 142 | Object up = upstream.ifCached(segment, s); 143 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 144 | } 145 | 146 | @Override 147 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 148 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15); 149 | Object up = upstream.ifCached(segment, s); 150 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 151 | } 152 | 153 | @Override 154 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 155 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16); 156 | Object up = upstream.ifCached(segment, s); 157 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 158 | } 159 | 160 | @Override 161 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 162 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17); 163 | Object up = upstream.ifCached(segment, s); 164 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 165 | } 166 | 167 | @Override 168 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 169 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18); 170 | Object up = upstream.ifCached(segment, s); 171 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 172 | } 173 | 174 | @Override 175 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 176 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19); 177 | Object up = upstream.ifCached(segment, s); 178 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 179 | } 180 | 181 | @Override 182 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 183 | ISeq s = ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20); 184 | Object up = upstream.ifCached(segment, s); 185 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 186 | } 187 | 188 | @Override 189 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 190 | Object[] allArgs = new Object[20 + args.length]; 191 | System.arraycopy(args, 0, allArgs, 20, args.length); 192 | allArgs[0] = arg1; 193 | allArgs[1] = arg2; 194 | allArgs[2] = arg3; 195 | allArgs[3] = arg4; 196 | allArgs[4] = arg5; 197 | allArgs[5] = arg6; 198 | allArgs[6] = arg7; 199 | allArgs[7] = arg8; 200 | allArgs[8] = arg9; 201 | allArgs[9] = arg10; 202 | allArgs[10] = arg11; 203 | allArgs[11] = arg12; 204 | allArgs[12] = arg13; 205 | allArgs[13] = arg14; 206 | allArgs[14] = arg15; 207 | allArgs[15] = arg16; 208 | allArgs[16] = arg17; 209 | allArgs[17] = arg18; 210 | allArgs[18] = arg19; 211 | allArgs[19] = arg20; 212 | ISeq s = ArraySeq.create(allArgs); 213 | Object up = upstream.ifCached(segment, s); 214 | return up == absent ? AFn.applyToHelper(segment.getF(), s) : up; 215 | } 216 | 217 | @Override 218 | public Object applyTo(ISeq arglist) { 219 | Object up = upstream.ifCached(segment, arglist); 220 | return up == absent ? AFn.applyToHelper(segment.getF(), arglist) : up; 221 | } 222 | 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /java/memento/mount/CachedMultiFn.java: -------------------------------------------------------------------------------- 1 | package memento.mount; 2 | 3 | import clojure.lang.*; 4 | import memento.base.ICache; 5 | import memento.base.Segment; 6 | 7 | public class CachedMultiFn extends MultiFn implements IMountPoint, Cached, IObj { 8 | private final Object reloadGuard; 9 | private final MultiFn originalFn; 10 | private final Segment segment; 11 | 12 | @Override 13 | public IPersistentMap asMap() { 14 | return mp.asMap(); 15 | } 16 | 17 | @Override 18 | public Object cached(ISeq args) { 19 | return mp.cached(args); 20 | } 21 | 22 | @Override 23 | public Object ifCached(ISeq args) { 24 | return mp.ifCached(args); 25 | } 26 | 27 | @Override 28 | public Object getTags() { 29 | return mp.getTags(); 30 | } 31 | 32 | @Override 33 | public Object handleEvent(Object event) { 34 | return mp.handleEvent(event); 35 | } 36 | 37 | @Override 38 | public ICache invalidate(ISeq args) { 39 | return mp.invalidate(args); 40 | } 41 | 42 | @Override 43 | public ICache invalidateAll() { 44 | return mp.invalidateAll(); 45 | } 46 | 47 | @Override 48 | public ICache mountedCache() { 49 | return mp.mountedCache(); 50 | } 51 | 52 | @Override 53 | public Segment segment() { 54 | return mp.segment(); 55 | } 56 | 57 | @Override 58 | public ICache addEntries(IPersistentMap argsToVals) { 59 | return mp.addEntries(argsToVals); 60 | } 61 | 62 | private final String name; 63 | private final IMountPoint mp; 64 | private final IPersistentMap meta; 65 | 66 | public CachedMultiFn(String name, Object reloadGuard, IMountPoint mp, IPersistentMap meta, MultiFn originalFn) { 67 | super(name, originalFn.dispatchFn, originalFn.defaultDispatchVal, originalFn.hierarchy); 68 | this.reloadGuard = reloadGuard; 69 | this.mp = mp; 70 | this.name = name; 71 | this.meta = meta; 72 | this.originalFn = originalFn; 73 | this.segment = mp.segment(); 74 | } 75 | 76 | @Override 77 | public IPersistentMap meta() { 78 | return meta; 79 | } 80 | 81 | @Override 82 | public IObj withMeta(IPersistentMap meta) { 83 | return new CachedMultiFn(name, reloadGuard, mp, meta, originalFn); 84 | } 85 | 86 | @Override 87 | public Object call() { 88 | return mp.mountedCache().cached(segment, ArraySeq.create()); 89 | } 90 | 91 | @Override 92 | public void run() { 93 | mp.mountedCache().cached(segment, ArraySeq.create()); 94 | } 95 | 96 | @Override 97 | public Object invoke() { 98 | return mp.mountedCache().cached(segment, ArraySeq.create()); 99 | } 100 | 101 | @Override 102 | public Object invoke(Object arg1) { 103 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1)); 104 | } 105 | 106 | @Override 107 | public Object invoke(Object arg1, Object arg2) { 108 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2)); 109 | } 110 | 111 | @Override 112 | public Object invoke(Object arg1, Object arg2, Object arg3) { 113 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3)); 114 | } 115 | 116 | @Override 117 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4) { 118 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4)); 119 | } 120 | 121 | @Override 122 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 123 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5)); 124 | } 125 | 126 | @Override 127 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6) { 128 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6)); 129 | } 130 | 131 | @Override 132 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7) { 133 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7)); 134 | } 135 | 136 | @Override 137 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8) { 138 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)); 139 | } 140 | 141 | @Override 142 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9) { 143 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)); 144 | } 145 | 146 | @Override 147 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10) { 148 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)); 149 | } 150 | 151 | @Override 152 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11) { 153 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11)); 154 | } 155 | 156 | @Override 157 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12) { 158 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12)); 159 | } 160 | 161 | @Override 162 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13) { 163 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13)); 164 | } 165 | 166 | @Override 167 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14) { 168 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14)); 169 | } 170 | 171 | @Override 172 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15) { 173 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15)); 174 | } 175 | 176 | @Override 177 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16) { 178 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16)); 179 | } 180 | 181 | @Override 182 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17) { 183 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17)); 184 | } 185 | 186 | @Override 187 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18) { 188 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18)); 189 | } 190 | 191 | @Override 192 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19) { 193 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19)); 194 | } 195 | 196 | @Override 197 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20) { 198 | return mp.mountedCache().cached(segment, ArraySeq.create(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20)); 199 | } 200 | 201 | @Override 202 | public Object invoke(Object arg1, Object arg2, Object arg3, Object arg4, Object arg5, Object arg6, Object arg7, Object arg8, Object arg9, Object arg10, Object arg11, Object arg12, Object arg13, Object arg14, Object arg15, Object arg16, Object arg17, Object arg18, Object arg19, Object arg20, Object... args) { 203 | Object[] allArgs = new Object[20 + args.length]; 204 | System.arraycopy(args, 0, allArgs, 20, args.length); 205 | allArgs[0] = arg1; 206 | allArgs[1] = arg2; 207 | allArgs[2] = arg3; 208 | allArgs[3] = arg4; 209 | allArgs[4] = arg5; 210 | allArgs[5] = arg6; 211 | allArgs[6] = arg7; 212 | allArgs[7] = arg8; 213 | allArgs[8] = arg9; 214 | allArgs[9] = arg10; 215 | allArgs[10] = arg11; 216 | allArgs[11] = arg12; 217 | allArgs[12] = arg13; 218 | allArgs[13] = arg14; 219 | allArgs[14] = arg15; 220 | allArgs[15] = arg16; 221 | allArgs[16] = arg17; 222 | allArgs[17] = arg18; 223 | allArgs[18] = arg19; 224 | allArgs[19] = arg20; 225 | return mp.mountedCache().cached(segment, ArraySeq.create(allArgs)); 226 | } 227 | 228 | @Override 229 | public Object applyTo(ISeq arglist) { 230 | return mp.mountedCache().cached(segment, arglist); 231 | } 232 | 233 | public IMountPoint getMp() { 234 | return mp; 235 | } 236 | 237 | public IFn getOriginalFn() { 238 | return originalFn; 239 | } 240 | 241 | @Override 242 | public String toString() { 243 | return "CachedMultiFn{" + 244 | "originalFn=" + originalFn + 245 | ", segment=" + segment + 246 | ", mp=" + mp + 247 | ", meta=" + meta + 248 | '}'; 249 | } 250 | 251 | public Segment getSegment() { 252 | return segment; 253 | } 254 | 255 | @Override 256 | public MultiFn addMethod(Object dispatchVal, IFn method) { 257 | originalFn.addMethod(dispatchVal, method); 258 | return this; 259 | } 260 | 261 | @Override 262 | public IFn getMethod(Object dispatchVal) { 263 | return originalFn.getMethod(dispatchVal); 264 | } 265 | 266 | @Override 267 | public IPersistentMap getMethodTable() { 268 | return originalFn == null ? PersistentHashMap.EMPTY : originalFn.getMethodTable(); 269 | } 270 | 271 | @Override 272 | public IPersistentMap getPreferTable() { 273 | return originalFn.getPreferTable(); 274 | } 275 | 276 | @Override 277 | public MultiFn preferMethod(Object dispatchValX, Object dispatchValY) { 278 | originalFn.preferMethod(dispatchValX, dispatchValY); 279 | return this; 280 | } 281 | 282 | @Override 283 | public MultiFn removeMethod(Object dispatchVal) { 284 | originalFn.removeMethod(dispatchVal); 285 | return this; 286 | } 287 | 288 | @Override 289 | public MultiFn reset() { 290 | originalFn.reset(); 291 | return this; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Memento 2 | 3 | A library for function memoization with scoped caches and tagged eviction capabilities. 4 | 5 | ## Dependency 6 | 7 | [](https://clojars.org/org.clojars.roklenarcic/memento) 8 | 9 | ## Version 2.0 breaking changes 10 | 11 | Version 2 moves from Java 8 to Java 11 as minimum JVM version. Caffeine version is 3 instead of 2. 12 | 13 | ## Version 1.0 breaking changes 14 | 15 | Version 1.0 represents a switch from Guava to Caffeine, which is a faster caching library, with added 16 | benefit of not pulling in the whole Guava artefact which is more that just that Cache. The Guava Cache type 17 | key and the config namespace are deprecated and will be removed in the future. 18 | 19 | ## Motivation 20 | 21 | Why is there a need for another caching library? 22 | 23 | - request scoped caching (and other scoped caching) 24 | - eviction by secondary index 25 | - disabling cache for specific function returns 26 | - tiered caching 27 | - size based eviction that puts limits around more than one function at the time 28 | - cache events 29 | 30 | ## Performance 31 | 32 | - [**Performance**](doc/performance.md) 33 | 34 | ## Adding cache to a function 35 | 36 | **With require `[memento.core :as m][memento.config :as mc]`:** 37 | 38 | Define a function + create new cache + attach cache to a function: 39 | 40 | ```clojure 41 | (m/defmemo my-function 42 | {::m/cache {mc/type mc/caffeine}} 43 | [x] 44 | (* 2 x)) 45 | ``` 46 | 47 | ### **The key parts here**: 48 | - `defmemo` works just like `defn` but wraps the function in a cache 49 | - specify the cache configuration via `:memento.core/cache` keyword in function meta 50 | 51 | Quick reminder, there are two ways to provide metadata when defining functions: `defn` allows a meta 52 | map to be provided before the argument list, or you can add meta to the symbol directly as supported by the reader: 53 | 54 | ```clojure 55 | (m/defmemo ^{::m/cache {mc/type mc/caffeine}} my-function 56 | [x] 57 | (* 2 x)) 58 | ``` 59 | 60 | ### Caching an anonymous function 61 | 62 | You can add cache to a function object (in `clojure.core/memoize` fashion): 63 | 64 | ```clojure 65 | (m/memo (fn [] ...) {mc/type mc/caffeine}) 66 | ``` 67 | 68 | ### Other ways to attach Cache to a function 69 | 70 | [Caches and memoize calls](doc/major.md) 71 | 72 | ## Cache conf(iguration) 73 | 74 | See above: `{mc/type mc/caffeine}` 75 | 76 | The cache conf is an open map of namespaced keywords such as `:memento.core/type`, various cache implementations can 77 | use implementation specific config keywords. 78 | 79 | Learning all the keywords and what they do can be hard. To assist you 80 | there are special conf namespaces provided where conf keywords are defined as vars with docs, 81 | so it's easy so you to see which configuration keys are available and what their function is. It also helps 82 | prevent bugs from typing errors. 83 | 84 | The core properties are defined in `[memento.config :as mc]` namespace. Caffeine specific properties are defined 85 | in `[memento.caffeine.config :as mcc]`. 86 | 87 | Here's a couple of equal ways of writing out you cache configuration meta: 88 | 89 | ```clojure 90 | ; the longest 91 | {:memento.core/cache {:memento.core/type :memento.core/caffeine}} 92 | ; using alias 93 | {::m/cache {::m/type ::m/caffeine}} 94 | ; using memento.config vars - recommended 95 | {mc/cache {mc/type mc/caffeine}} 96 | ``` 97 | 98 | ### Core conf 99 | 100 | The core configuration properties: 101 | 102 | #### mc/type 103 | 104 | Cache implementation type, e.g. caffeine, redis, see the implementation library docs. **Make sure 105 | you load the implementation namespace at some point!**. Caffeine namespace is loaded automatically 106 | when memento.core is loaded. 107 | 108 | #### mc/size< 109 | 110 | Size limit expressed in number of entries or total weight if implementation supports weighted cache entries 111 | 112 | #### mc/ttl 113 | 114 | Entry is invalid after this amount of time has passed since its creation 115 | 116 | It's either a number (of seconds), a pair describing duration e.g. `[10 :m]` for 10 minutes, 117 | see `memento.config/timeunits` for timeunits. 118 | 119 | #### mc/fade 120 | 121 | Entry is invalid after this amount of time has passed since last access, see `mc/ttl` for duration 122 | specification. 123 | 124 | #### mc/key-fn, mc/key-fn* 125 | 126 | Specify a function that will transform the function arg list into the final cache key. Used 127 | to drop function arguments that shouldn't factor into cache tag equality. 128 | 129 | The `key-fn` receives a sequence of arguments, `key-fn*` receives multiple arguments as if it 130 | was the function itself. 131 | 132 | See: [Changing the key for cached tag](doc/key-fn.md) 133 | 134 | #### mc/ret-fn 135 | 136 | A function that is called on every cached function return value. Used for general transformations 137 | of return values. 138 | 139 | #### mc/ret-ex-fn 140 | 141 | A function that is called on every thrown Throwable. Used for general transformations 142 | of thrown exceptions values. 143 | 144 | #### mc/seed 145 | 146 | Initial entries to load in the cache. 147 | 148 | #### mc/initial-capacity 149 | 150 | Cache capacity hint to implementation. 151 | 152 | ## Conf is a value (map) 153 | 154 | Cache conf can get quite involved: 155 | 156 | ```clojure 157 | (ns memento.tryout 158 | (:require [memento.core :as m] 159 | ; general cache conf keys 160 | [memento.config :as mc] 161 | ; caffeine specific cache conf keys 162 | [memento.caffeine.config :as mcc])) 163 | 164 | (def my-weird-cache 165 | "Conf for caffeine cache that caches up to 20 seconds and up to 30 entries, uses weak 166 | references and prints when keys get evicted." 167 | {mc/type mc/caffeine 168 | mc/size< 30 169 | mc/ttl 20 170 | mcc/weak-values true 171 | mcc/removal-listener #(println (apply format "Function %s key %s, value %s got evicted because of %s" %&))}) 172 | 173 | (m/defmemo my-function 174 | {::m/cache my-weird-cache} 175 | [x] (* 2 x)) 176 | ``` 177 | 178 | Seeing as cache conf is a map, I recommend a pattern where you have a namespace in your application that contains vars 179 | with your commonly used cache conf maps and functions that generate slightly parameterized 180 | configuration. E.g. 181 | 182 | ```clojure 183 | (ns my-project.cache 184 | (:require [memento.config :as mc])) 185 | 186 | ;; infinite cache 187 | (def inf-cache {mc/type mc/caffeine}) 188 | 189 | (defn for-seconds [n] (assoc inf-cache mc/ttl n)) 190 | ``` 191 | 192 | Then you just use that in your code: 193 | 194 | ```clojure 195 | (m/defmemo my-function 196 | {::m/cache (cache/for-seconds 60)} 197 | [x] (* x 2)) 198 | ``` 199 | 200 | ## Caches and mount points 201 | 202 | Enabling memoization of a function is composed of two distinct steps: 203 | 204 | - creating a Cache (optional, as you can use an existing cache) 205 | - binding the cache to the function (a MountPoint is used to connect a function being memoized to the cache) 206 | 207 | A cache, an instance of memento.base/Cache, can contain entries from multiple functions and can be shared between memoized functions. 208 | Each memoized function is bound to a Cache via MountPoint. When you call a function such as `(m/as-map a-cached-function)` you are 209 | operating on a MountPoint. 210 | 211 | The reason for this separation is two-fold: 212 | 213 | #### 1. **Improved Size Based Eviction** 214 | 215 | So far all examples implicitly created a new cache for each memoized function, but if we use same cache for multiple 216 | functions, then any size based eviction will apply to them as a whole. If you have 100 memoized functions, and you want to 217 | somewhat limit their memory use, what do you do? In a typical cache library you might limit each of them to 100 entries. So you 218 | allocated 10000 slots total, but one function might have an empty cache, while a very heavily used one needs way more than 100 219 | slots. If all 100 function are backed by same Cache instance with 10000 slots then they automatically balance themselves out. 220 | 221 | #### 2. **Changing cache temporarily to allow for scoped caching** 222 | 223 | This indirection with Mount Points allows us to change which cache is backing a function dynamically. See discussion of tagged 224 | caches below. Here's an example of using tags when caching and scoped caching 225 | 226 | ```clojure 227 | (ns myproject.some-ns 228 | (:require [myproject.cache :as cache] 229 | [memento.core :as m])) 230 | 231 | (defn get-person-by-id [person-id] 232 | (let [person (db/get-person person-id)] 233 | ; tag the returned object with :person + id pair 234 | (m/with-tag-id person :person (:id person)))) 235 | 236 | ; add a cache to the function with tags :person and :request 237 | (m/memo #'get-person-by-id [:person :request] cache/inf) 238 | 239 | ; remove cache entries from every cache tagged :person globally, where the 240 | ; tag is tagged with :person 1 241 | (m/memo-clear-tag! :person 1) 242 | 243 | (m/with-caches :request (constantly (m/create cache/inf)) 244 | ; inside this block, a fresh new cache is used (and discarded) 245 | ; making a scope-like functionality 246 | (get-person-by-id 5)) 247 | ``` 248 | 249 | ## Variable expiry 250 | 251 | Instead of setting a fixed duration of validity for entries in a cache, it is possible 252 | to set these duration on per-tag or per-mount point basis. 253 | 254 | Note that for Caffeine cache variable expiry caching is somewhat slower. 255 | 256 | ### **Read [here](doc/variable-expiry.md)** 257 | 258 | ## Additional features 259 | 260 | #### [Prevent caching of a specific return value (and general return value xform)](doc/ret-fn.md) 261 | #### [Manually add or evict entries](doc/manual-add-remove.md) 262 | 263 | #### `(m/as-map memoized-function)` to get a map of cache entries, also works on MountPoint instances 264 | #### `(m/memoized? a-function)` returns true if the function is memoized 265 | #### `(m/memo-unwrap memoized-function)` returns original uncached function, also works on MountPoint instances 266 | #### `(m/active-cache memoized-function)` returns Cache instance from the function, if present. 267 | 268 | ## Tags 269 | 270 | You can add tags to the caches. Tags enable that you: 271 | 272 | - run actions on caches with specific tags 273 | - **change or update cache of tagged MountPoints within a scope** 274 | - change or update cache of tagged MountPoints permanently 275 | - use secondary index to invalidate entries by a tag + ID pair 276 | 277 | This is a very powerful feature, [read more here.](doc/tags.md) 278 | 279 | ## Loads and invalidations 280 | 281 | Cache only has a single ongoing load for a key going at any one time. For Caffeine cache, if a key is invalidated 282 | during the load, the load is repeated. This is the only way you can get multiple function invocations happen for a single 283 | cached function call. When an tag is invalidated while it's being loaded, the Thread that loads it will be interrupted. 284 | 285 | ## Namespace scan 286 | 287 | You can scan loaded namespaces for annotated vars and automatically create caches. 288 | 289 | [Read more](doc/ns-scan.md) 290 | 291 | ## Events 292 | 293 | You can fire an event at a memoized function. Main use case is to enable adding entries to different functions from same data. 294 | 295 | [Read more](doc/events.md) 296 | 297 | ## Tiered caching 298 | 299 | You can use caches that combine two other caches in some way. The easiest way to generate 300 | the cache configuration needed is to use `memento.core/tiered`,`memento.core/consulting`, `memento.core/daisy`. 301 | 302 | [Read more](doc/tiered.md) 303 | 304 | ## if-cached 305 | 306 | memento.core/if-cache is like an if-let, but the "then" branch executes if the function call 307 | is cached, otherwise else branch is executed. The binding is expected to be a cached function call form, otherwise 308 | an error is thrown. 309 | 310 | Example: 311 | 312 | ```clojure 313 | (if-cached [v (my-function arg1)] 314 | (println "cached value is " v) 315 | (println "value is not cached")) 316 | ``` 317 | 318 | ## Skip/disable caching 319 | 320 | If you set `-Dmemento.enabled=false` JVM option (or change `memento.config/enabled?` var root binding), 321 | then type of all caches created will be `memento.base/no-cache`, which does no caching. 322 | 323 | ## Reload guards 324 | 325 | When you memoize a function with tags, a special object is created that will clean up in internal tag 326 | mappings when memoized function is GCed. It's important when reloading namespaces to remove mount points 327 | on the old function versions. 328 | 329 | It uses finalize, which isn't free (takes extra work to allocate and GC has to work harder), so 330 | if you don't use namespace reloading, and you want to optimize you can disable reload guard objects. 331 | 332 | Set `-Dmemento.reloadable=false` JVM option (or change `memento.config/reload-guards?` var root binding). 333 | 334 | ## Breaking changes 335 | 336 | Patch versions are compatible. Minor version change breaks API for implementation authors, but not for users, 337 | major version change breaks user API. 338 | 339 | Version 1.0.x changed implementation from Guava to Caffeine 340 | Version 0.9.0 introduced many breaking changes. 341 | 342 | ## License 343 | 344 | Copyright © 2020-2021 Rok Lenarčič 345 | 346 | Licensed under the term of the MIT License, see LICENSE. 347 | -------------------------------------------------------------------------------- /test/memento/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns memento.core-test 2 | (:require [clojure.test :refer :all] 3 | [memento.core :as m :refer :all] 4 | [memento.config :as mc] 5 | [memento.caffeine.config :as mcc]) 6 | (:import (java.io IOException) 7 | (memento.base EntryMeta ICache) 8 | (memento.caffeine Expiry) 9 | (memento.mount IMountPoint))) 10 | 11 | (def inf {mc/type mc/caffeine}) 12 | (defn size< [max-size] 13 | (assoc inf mc/size< max-size)) 14 | (defn ret-fn [f] 15 | (assoc inf mc/ret-fn f)) 16 | 17 | (def id (memo identity inf)) 18 | 19 | (defn- check-core-features 20 | [factory] 21 | (let [mine (factory identity) 22 | them (memoize identity)] 23 | (testing "That the memo function works the same as core.memoize" 24 | (are [x y] (= x y) 25 | (mine 42) (them 42) 26 | (mine ()) (them ()) 27 | (mine []) (them []) 28 | (mine #{}) (them #{}) 29 | (mine {}) (them {}) 30 | (mine nil) (them nil))) 31 | (testing "That the memo function has a proper cache" 32 | (is (memoized? mine)) 33 | (is (not (memoized? them))) 34 | (is (= 42 (mine 42))) 35 | (is (not (empty? (into {} (as-map mine))))) 36 | (is (memo-clear! mine)) 37 | (is (empty? (into {} (as-map mine)))))) 38 | (testing "That the cache retries in case of exceptions" 39 | (let [access-count (atom 0) 40 | f (factory 41 | (fn [] 42 | (swap! access-count inc) 43 | (throw (IllegalArgumentException.))))] 44 | (is (thrown? IllegalArgumentException (f))) 45 | (is (thrown? IllegalArgumentException (f))) 46 | (is (= 2 @access-count)))) 47 | (testing "That the memo function does not have a race condition" 48 | (let [access-count (atom 0) 49 | slow-identity 50 | (factory (fn [x] 51 | (swap! access-count inc) 52 | (Thread/sleep 100) 53 | x))] 54 | (every? identity (pvalues (slow-identity 5) (slow-identity 5))) 55 | (is (= @access-count 1)))) 56 | (testing "That exceptions are correctly unwrapped." 57 | (is (thrown? ClassNotFoundException ((factory (fn [] (throw (ClassNotFoundException.))))))) 58 | (is (thrown? IllegalArgumentException ((factory (fn [] (throw (IllegalArgumentException.)))))))) 59 | (testing "Null return caching." 60 | (let [access-count (atom 0) 61 | mine (factory (fn [] (swap! access-count inc) nil))] 62 | (is (nil? (mine))) 63 | (is (nil? (mine))) 64 | (is (= @access-count 1))))) 65 | 66 | (deftest test-memo (check-core-features #(memo % inf))) 67 | 68 | (deftest test-lru 69 | (let [mine (memo identity (size< 2))] 70 | ;; First check that the basic memo behavior holds 71 | (check-core-features #(memo % (size< 2))) 72 | 73 | ;; Now check FIFO-specific behavior 74 | (testing "that when the limit threshold is not breached, the cache works like the basic version" 75 | (are [x y] = 76 | 42 (mine 42) 77 | {[42] 42} (as-map mine) 78 | 43 (mine 43) 79 | {[42] 42, [43] 43} (as-map mine) 80 | 42 (mine 42) 81 | {[42] 42, [43] 43} (as-map mine))) 82 | (testing "that when the limit is breached, the oldest value is dropped" 83 | (are [x y] = 84 | 44 (mine 44) 85 | {[44] 44, [43] 43} (as-map mine))))) 86 | 87 | 88 | (deftest test-ttl 89 | ;; First check that the basic memo behavior holds 90 | (check-core-features #(memo % (assoc inf mc/ttl 2))) 91 | 92 | ;; Now check TTL-specific behavior 93 | (let [mine (memo identity (assoc inf mc/ttl [2 :s]))] 94 | (are [x y] = 95 | 42 (mine 42) 96 | {[42] 42} (as-map mine)) 97 | (Thread/sleep 3000) 98 | (are [x y] = 99 | 43 (mine 43) 100 | {[43] 43} (as-map mine))) 101 | 102 | (let [mine (memo identity (assoc inf mc/ttl [5 :ms])) 103 | limit 2000000 104 | start (System/currentTimeMillis)] 105 | (loop [n 0] 106 | (if-not (mine 42) 107 | (do 108 | (is false (str "Failure on call " n))) 109 | (if (< n limit) 110 | (recur (+ 1 n))))) 111 | (println "ttl test completed" limit "calls in" 112 | (- (System/currentTimeMillis) start) "ms"))) 113 | 114 | (deftest test-memoization-utils 115 | (let [CACHE_IDENTITY (:memento.mount/mount (meta id))] 116 | (testing "that the stored cache is not null" 117 | (is (instance? IMountPoint id))) 118 | (testing "that a populated function looks correct at its inception" 119 | (is (memoized? id)) 120 | (is (instance? ICache (active-cache id))) 121 | (is (as-map id)) 122 | (is (empty? (as-map id)))) 123 | (testing "that a populated function looks correct after some interactions" 124 | ;; Memoize once 125 | (is (= 42 (id 42))) 126 | ;; Now check to see if it looks right. 127 | (is (find (as-map id) '(42))) 128 | (is (= 1 (count (as-map id)))) 129 | ;; Memoize again 130 | (is (= [] (id []))) 131 | (is (find (as-map id) '([]))) 132 | (is (= 2 (count (as-map id)))) 133 | (testing "that upon memoizing again, the cache should not change" 134 | (is (= [] (id []))) 135 | (is (find (as-map id) '([]))) 136 | (is (= 2 (count (as-map id))))) 137 | (testing "if clearing the cache works as expected" 138 | (is (memo-clear! id)) 139 | (is (empty? (as-map id))))) 140 | (testing "that after all manipulations, the cache maintains its identity" 141 | (is (identical? CACHE_IDENTITY (:memento.mount/mount (meta id))))) 142 | (testing "that a cache can be seeded and used normally" 143 | (memo-clear! id) 144 | (is (memo-add! id {[42] 42})) 145 | (is (= 42 (id 42))) 146 | (is (= {[42] 42} (as-map id))) 147 | (is (= 108 (id 108))) 148 | (is (= {[42] 42 [108] 108} (as-map id))) 149 | (is (memo-add! id {[111] nil [nil] 111})) 150 | (is (= 111 (id nil))) 151 | (is (= nil (id 111))) 152 | (is (= {[42] 42 [108] 108 [111] nil [nil] 111} (as-map id)))) 153 | (testing "that we can get back the original function" 154 | (is (memo-clear! id)) 155 | (is (memo-add! id {[42] 24})) 156 | (is (= 24 (id 42))) 157 | (is (= 42 ((memo-unwrap id) 42)))))) 158 | 159 | (deftest memo-with-seed-cmemoize-18 160 | (let [mine (memo identity (assoc inf mc/seed {[42] 99}))] 161 | (testing "that a memo seed works" 162 | (is (= 41 (mine 41))) 163 | (is (= 99 (mine 42))) 164 | (is (= 43 (mine 43))) 165 | (is (= {[41] 41, [42] 99, [43] 43} (as-map mine)))))) 166 | 167 | (deftest memo-with-dropped-args 168 | ;; must use var to preserve metadata 169 | (let [mine (memo + (assoc inf mc/key-fn rest))] 170 | (testing "that key-fnb collapses the cache key space" 171 | (is (= 13 (mine 1 2 10))) 172 | (is (= 13 (mine 10 2 1))) 173 | (is (= 13 (mine 10 2 10))) 174 | (is (= {[2 10] 13, [2 1] 13} (as-map mine)))))) 175 | 176 | (def test-atom (atom 0)) 177 | (defn test-var-fn [x] (swap! test-atom inc) (* x 3)) 178 | 179 | (deftest add-memo-to-var 180 | (testing "that memoing a var works" 181 | (memo #'test-var-fn inf) 182 | (is (= 3 (test-var-fn 1))) 183 | (is (= 3 (test-var-fn 1))) 184 | (is (= 3 (test-var-fn 1))) 185 | (is (= @test-atom 1)))) 186 | 187 | (deftest seed-test 188 | (testing "that seeding a function works" 189 | (let [cached (memo + (assoc inf mc/seed {[3 5] 100 [4 5] 2000}))] 190 | (is (= 50 (cached 20 30))) 191 | (is (= 1 (cached -1 2))) 192 | (is (= 100 (cached 3 5))) 193 | (is (= 2000 (cached 4 5)))))) 194 | 195 | (deftest key-fn-test 196 | (testing "that key-fn works for direct cache" 197 | (let [cached (memo (fn [& ids] ids) (assoc inf mc/key-fn set))] 198 | (is (= [3 2 1] (cached 3 2 1))) 199 | (is (= [3 2 1] (cached 1 2 3))) 200 | (is (= [3 2 1] (cached 1 3 3 2 2 2))) 201 | (is (= [2 1] (cached 2 1)))))) 202 | 203 | (deftest key-fn*-test 204 | (testing "that key-fn works for direct cache" 205 | (let [cached (memo (fn [& ids] ids) (assoc inf mc/key-fn* hash-set))] 206 | (is (= [3 2 1] (cached 3 2 1))) 207 | (is (= [3 2 1] (cached 1 2 3))) 208 | (is (= [3 2 1] (cached 1 3 3 2 2 2))) 209 | (is (= [2 1] (cached 2 1)))))) 210 | 211 | (deftest ret-fn-non-cached 212 | (testing "that ret-fn is ran" 213 | (is (= -4 ((memo + (ret-fn #(* -1 %2))) 2 2))) 214 | (is (= true ((memo (constantly nil) (ret-fn #(nil? %2))) 1))) 215 | (is (= nil ((memo + (ret-fn (constantly nil))) 2 2)))) 216 | (testing "that non-cached is respected" 217 | (let [access-nums (atom []) 218 | f (memo 219 | (fn [number] 220 | (swap! access-nums conj number) 221 | (if (zero? (mod number 3)) (do-not-cache number) number)) 222 | (ret-fn #(if (and (number? %2) (zero? (mod %2 5))) (do-not-cache %2) %2)))] 223 | (is (= (range 20) (map f (range 20)))) 224 | (is (= (range 20) (map f (range 20)))) 225 | (is (= (concat (range 20) [0 3 5 6 9 10 12 15 18]) @access-nums))))) 226 | 227 | (deftest get-tags-test 228 | (testing "tags get returned" 229 | (let [cached (memo identity :person) 230 | cached2 (memo identity [:actor :dog]) 231 | cached3 (memo identity {mc/tags :x})] 232 | (is (= [:person] (tags cached))) 233 | (is (= [:actor :dog] (tags cached2))) 234 | (is (= [:x] (tags cached3)))))) 235 | 236 | (deftest with-caches-test 237 | (testing "a different cache is used within the block" 238 | (let [access-nums (atom []) 239 | f (memo (fn [number] (swap! access-nums conj number)) :person inf)] 240 | (is (= [10] (f 10))) 241 | (is (= [10] (f 10))) 242 | (is (= [10 20] (f 20))) 243 | (is (= [10 20] (f 20))) 244 | (is (= [10 20] @access-nums)) 245 | (with-caches :person (constantly (create inf)) 246 | (is (= [10 20 10] (f 10))) 247 | (is (= [10 20 10] (f 10))) 248 | (is (= [10 20 10 30] (f 30))) 249 | (is (= [10 20 10 30] @access-nums))) 250 | (is (= [10] (f 10))) 251 | (is (= [10 20 10 30 30] (f 30)))))) 252 | 253 | (deftest update-tag-caches-test 254 | (testing "changes cache root binding" 255 | (let [access-nums (atom 0) 256 | f (memo (fn [number] (swap! access-nums + number)) :person inf)] 257 | (is (= 10 (f 10))) 258 | (is (= 10 (f 10))) 259 | (is (= 10 @access-nums)) 260 | (update-tag-caches! :person (constantly (create inf))) 261 | (is (= 20 (f 10))) 262 | (is (= 20 @access-nums)) 263 | (with-caches :person (constantly (create inf)) 264 | (is (= 30 (f 10))) 265 | (is (= 30 (f 10))) 266 | (is (= 30 @access-nums)) 267 | (update-tag-caches! :person (constantly (create inf))) 268 | (is (= 40 (f 10))) 269 | (is (= 40 @access-nums))) 270 | (is (= 20 (f 10))) 271 | (is (= 40 @access-nums)) 272 | (update-tag-caches! :person (constantly (create inf))) 273 | (is (= 50 (f 10))) 274 | (is (= 50 @access-nums))))) 275 | 276 | (deftest tagged-eviction-test 277 | (testing "adding tag ID info" 278 | (is (= (EntryMeta. 1 false #{[:person 55]}) 279 | (-> 1 (with-tag-id :person 55)))) 280 | (is (= (EntryMeta. 1 true #{[:person 55] [:account 6]}) 281 | (-> 1 (with-tag-id :person 55) (with-tag-id :account 6) do-not-cache)))) 282 | (testing "tagged eviction" 283 | (let [f (memo (fn [x] (with-tag-id x :tag x)) :tag inf)] 284 | (is (= {} (as-map f))) 285 | (is (= {[1] 1} (do (f 1) (as-map f)))) 286 | (is (= {[1] 1 [2] 2} (do (f 2) (as-map f)))) 287 | (is (= {[2] 2} (do (memo-clear-tag! :tag 1) (as-map f))))))) 288 | 289 | (deftest fire-event-test 290 | (testing "event is fired on referenced cache" 291 | (let [access-nums (atom 0) 292 | inner-f (fn [x] (swap! access-nums inc) x) 293 | evt-f (fn [this evt] 294 | (m/memo-add! this {[evt] (inc evt)})) 295 | x (m/memo inner-f {mc/type mc/caffeine mc/evt-fn evt-f mc/tags [:a]}) 296 | y (m/memo inner-f {mc/type mc/caffeine mc/evt-fn evt-f})] 297 | (is (= 1 (x 1))) 298 | (is (= 1 (x 1))) 299 | (is (= 1 @access-nums)) 300 | (m/fire-event! x 4) 301 | (m/fire-event! :a 5) 302 | (m/fire-event! y 6) 303 | (is (= {[1] 1 304 | [4] 5 305 | [5] 6} (m/as-map x))) 306 | (is (= {[6] 7} (m/as-map y))) 307 | (is (= 5 (x 4))) 308 | (is (= 6 (x 5))) 309 | (is (= 7 (y 6))) 310 | (is (= 1 @access-nums))))) 311 | 312 | (deftest if-cached-test 313 | (testing "if-cached executes then when cached" 314 | (let [x (m/memo identity {mc/type mc/caffeine})] 315 | (x 2) 316 | (is (= 2 317 | (m/if-cached [y (x 2)] 318 | y 319 | (throw (ex-info "Shouldn't throw" {}))))))) 320 | (testing "if-cached executes else when not cached" 321 | (let [x (m/memo identity {mc/type mc/caffeine})] 322 | (is (= ::none 323 | (m/if-cached [y (x 2)] 324 | (throw (ex-info "Shouldn't throw" {})) 325 | ::none)))))) 326 | 327 | (deftest put-during-load-test 328 | (testing "adding entries during load" 329 | (let [c (m/create inf) 330 | fn1 (m/memo identity {} c) 331 | fn2 (m/memo (fn [x] (m/memo-add! fn1 {[x] (inc x)}) 332 | (dec x)))] 333 | (is (= 4 (fn2 5))) 334 | (is (= 6 (fn1 5)))))) 335 | 336 | (defn fib [x] (if (<= x 1) 1 (+ (fib (- x 2)) (fib (dec x))))) 337 | 338 | (memo #'fib inf) 339 | 340 | (defn recursive [x] (recursive x)) 341 | 342 | (memo #'recursive inf) 343 | 344 | (deftest recursive-test 345 | (testing "recursive loads" 346 | (is (= 20365011074 (fib 50))) 347 | (is (thrown? StackOverflowError (recursive 1))))) 348 | 349 | (deftest concurrent-load 350 | (testing "concurrent test" 351 | (let [cnt (atom 0) 352 | f (m/memo (fn [x] 353 | (Thread/sleep 1000) 354 | (swap! cnt inc) x) 355 | inf) 356 | v (doall (repeatedly 5 #(future (f 1))))] 357 | (is (= [1 1 1 1 1] (mapv deref v)))))) 358 | 359 | (deftest vectors-key-fn* 360 | (testing "vectors don't throw exception when used with key-fn*" 361 | (let [c (m/memo identity (assoc inf mc/key-fn* identity))] 362 | (is (some? (m/memo-add! c {[1] 2})))))) 363 | 364 | (deftest invalidation-during-load-test 365 | (testing "bulk invalidation test" 366 | (let [a (atom 0) 367 | c (m/memo (fn [] (Thread/sleep 300) 368 | (m/with-tag-id (swap! a inc) :xx 1)) 369 | (assoc inf mc/tags :xx))] 370 | (future (Thread/sleep 15) 371 | (m/memo-clear-tag! :xx 1)) 372 | (is (= 2 (c))))) 373 | (testing "Invalidation during load test" 374 | (let [a (atom 0) 375 | after (atom 0) 376 | c (m/memo (fn [] (let [r (swap! a inc)] 377 | (Thread/sleep 300) 378 | [r (swap! after inc)])) inf)] 379 | (future (Thread/sleep 10) 380 | (m/memo-clear! c)) 381 | (is (= [2 1] (c)))))) 382 | 383 | (deftest ret-ex-fn-test 384 | (testing "returns transformed-exception" 385 | (let [e (RuntimeException.) 386 | c (m/memo (fn [] (Thread/sleep 100) 387 | (throw (IOException.))) 388 | (assoc inf mc/ret-ex-fn (fn [_ ee] (when (instance? IOException ee) e)))) 389 | f1 (future (try (c) (catch Exception e e))) 390 | f2 (future (try (c) (catch Exception e e)))] 391 | (is (= e @f1)) 392 | (is (= e @f2))))) 393 | 394 | (deftest variable-expiry-test 395 | (testing "Variable expiry" 396 | (let [c (m/memo 397 | identity 398 | (assoc inf mcc/expiry 399 | (reify Expiry 400 | (ttl [this _ k v] v) 401 | (fade [this _ k v]))))] 402 | (c 1) 403 | (c 2) 404 | (c 3) 405 | (Thread/sleep 1100) 406 | (is (= {'(2) 2 '(3) 3} (m/as-map c))))) 407 | (testing "Variable expiry fade" 408 | (let [c (m/memo 409 | identity 410 | (assoc inf mcc/expiry 411 | (reify Expiry 412 | (ttl [this _ k v] ) 413 | (fade [this _ k v] v))))] 414 | (c 1) 415 | (c 2) 416 | (c 3) 417 | (Thread/sleep 1100) 418 | (is (= {'(2) 2 '(3) 3} (m/as-map c))))) 419 | (testing "variable expiry via meta" 420 | (let [c (m/memo 421 | #(with-meta {} {mc/ttl (long (+ 1 %))}) 422 | (assoc inf mcc/expiry mcc/meta-expiry))] 423 | (c 1) 424 | (c 2) 425 | (c 3) 426 | (Thread/sleep 1100) 427 | (is (= {'(1) {} '(2) {} '(3) {}} (m/as-map c))) 428 | (Thread/sleep 1000) 429 | (is (= {'(2) {} '(3) {}} (m/as-map c)))))) --------------------------------------------------------------------------------