Skip to content

Commit

Permalink
Update README.md with latest changes
Browse files Browse the repository at this point in the history
  • Loading branch information
iarenaza committed Apr 7, 2021
1 parent f8c78d9 commit bccdca5
Showing 1 changed file with 88 additions and 31 deletions.
119 changes: 88 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,105 @@
# uk.me.rkd.ttlcache
[![Build Status](https://travis-ci.com/magnetcoop/ttlcache.svg?branch=master)](https://travis-ci.com/magnetcoop/ttlcache)
[![Clojars Project](https://img.shields.io/clojars/v/coop.magnet/ttlcache.svg)](https://clojars.org/coop.magnet/ttlcache)

A Clojure library designed to improve upon core.cache's TTLCache for some use cases. Specifically, it allows cache expiry in less than O(N) time (improving performance when small numbers of entries are expiring), and it allows per-item TTLs rather than a fixed TTL for the whole cache, which is useful for applications like DNS caching or [HTTP caching](https://developers.google.com/speed/articles/caching) where protocol responses specify a TTL.
# coop.magnet.ttlcache - forked from uk.me.rkd.ttlcache

Based on [data.priority-map](https://github.com/clojure/data.priority-map) and [core.cache](https://github.com/clojure/core.cache).
A Clojure library designed to improve upon core.cache's TTLCache for
some use cases. Specifically, it allows cache expiry in less than O(N)
time (improving performance when small numbers of entries are
expiring), and it allows per-item TTLs rather than a fixed TTL for the
whole cache, which is useful for applications like DNS caching or
[HTTP caching](https://developers.google.com/speed/articles/caching)
where protocol responses specify a TTL.

## Usage
Based on [data.priority-map](https://github.com/clojure/data.priority-map)
and [core.cache](https://github.com/clojure/core.cache).

Leiningen: `[uk.me.rkd.ttlcache "0.1.0"]` ([Clojars](https://clojars.org/uk.me.rkd.ttlcache))
## Installation

:require: `[uk.me.rkd.ttlcache :refer [per-item-ttl-cache-factory ttl-cache-factory]]`
[![Clojars Project](https://clojars.org/coop.magnet/ttlcache/latest-version.svg)](https://clojars.org/coop.magnet/ttlcache)

`uk.me.rkd.ttlcache` contains two public functions, both factory functions returning an instance of [CacheProtocol](https://github.com/clojure/core.cache/wiki/Extending):
## Usage
```clojure
(require '[coop.magnet.ttlcache :refer [per-item-ttl-cache-factory ttl-cache-factory]])
```

* `ttl-cache-factory` is designed as a drop-in replacement for the core.cache one, specifying a fixed TTL for the whole cache. See [the core.cache docs](https://github.com/clojure/core.cache/wiki/TTL) for more.
* `per-item-ttl-cache-factory` creates a PerItemTTLCache instance. It takes two arguments - a map of values to seed the cache with, and a function which is applied to the key and value of any added entries, and returns the TTL in seconds (`ttl-cache-factory` is built in this factory, and just supplies `(constantly n`) as the function).
`coop.magnet.ttlcache` contains two public functions, both factory
functions returning an instance of
[CacheProtocol](https://github.com/clojure/core.cache/wiki/Extending):

* `ttl-cache-factory` is designed as a drop-in replacement for the
core.cache one, specifying a fixed TTL (in milliseconds) for the
whole cache. See [the core.cache
docs](https://github.com/clojure/core.cache/wiki/TTL) for more.
* `per-item-ttl-cache-factory` creates a PerItemTTLCache instance. It
takes two arguments:
* a map of values to seed the cache with,
* and a function which is applied to the key and value of any added
entries, and returns the TTL in milli-seconds for the
entries. `ttl-cache-factory` is built in this factory, and just
supplies `(constantly n`) as the function.

The best example is probably the tests:

```clojure
(testing "TTL cache does not return a value that has expired."
(let [C (per-item-ttl-cache-factory {} :ttl-getter (fn [key value] (:ttl value)))]
(is (nil? (-> C
(assoc :a {:val 1 :ttl 500})
(sleepy 700)
(. lookup :a))))))
user> (require '[coop.magnet.ttlcache :refer [per-item-ttl-cache-factory ttl-cache-factory]]
'[clojure.test :refer [testing is]])
nil
user> (def sleepy #(do (Thread/sleep %2) %))
#'user/sleepy
user> (testing "TTL cache does not return a value that has expired."
(let [cache-values {:a {:val 1 :ttl 900}
:b {:val 2 :ttl 500}}
ttlcache (per-item-ttl-cache-factory cache-values :ttl-getter (fn [key value] (:ttl value)))]
(is (= {:val 1 :ttl 900}
(-> ttlcache
(sleepy 700)
(. lookup :a))))
(is (nil? (-> ttlcache
(sleepy 700)
(. lookup :b))))))
true
user>
```

## Big-O complexity

The TTLCache in core.cache uses a pair of maps - one to hold the values and another to hold the TTLs. This means most operations are quite efficient, but expiring items from the cache requires iterating over the whole map, so happens in O(N) time.

This TTLCache uses [data.priority-map](https://github.com/clojure/data.priority-map) to store the TTLs in a priority queue, with O(1) find-min and O(log N) delete-min.

Lookup, insertion and manual cache eviction basically equate to get/assoc/dissoc on a Clojure priority map, so take O(log n) time (marginally worse than O(log32 n) for core.cache, but nonetheless very scalable).

TTL-based expiry (done as part of adding to the cache in both implementations), however, only takes O(M log N) time, where M is the number of items evicted and N is the number of items in the cache. This means it performs better than the core.cache implementation when small numbers of items are being expired at once - in the best case, where no items expire, it is O(1) (just a heap peek).

Expiry of large numbers of items at once is less efficient than core.cache - obviously the scalability of O(M log N) gets closer to O(N) as M gets larger, but also because large numbers of operations on the pure-Clojure priority map perform worse than operations on an ordinary Java-based `transient`-able map. I hope to be able to improve this by using a Java-based persistent heap (e.g. jc-pheap).
In versions prior to 0.6 (when this library was developed), the
TTLCache in core.cache uses a pair of maps - one to hold the values
and another to hold the TTLs. This means most operations were quite
efficient, but expiring items from the cache requires iterating over
the whole map, so happens in O(N) time.

This TTLCache uses
[data.priority-map](https://github.com/clojure/data.priority-map) to
store the TTLs in a priority queue, with O(1) find-min and O(log N)
delete-min.

Lookup, insertion and manual cache eviction basically equate to
get/assoc/dissoc on a Clojure priority map, so take O(log n) time
(marginally worse than O(log32 n) for core.cache, but nonetheless very
scalable).

TTL-based expiry (done as part of adding to the cache in both
implementations), however, only takes O(M log N) time, where M is the
number of items evicted and N is the number of items in the
cache. This means it performs better than the core.cache
implementation at the time, when small numbers of items are being
expired at once - in the best case, where no items expire, it is O(1)
(just a heap peek).

Expiry of large numbers of items at once is less efficient than
core.cache - obviously the scalability of O(M log N) gets closer to
O(N) as M gets larger, but also because large numbers of operations on
the pure-Clojure priority map perform worse than operations on an
ordinary Java-based `transient`-able map.

## Tests vs core.cache

```
$ lein test
lein test uk.me.rkd.core-cache-comparison-test
lein test coop.magnet.core-cache-comparison-test
With 0 out of 5,000 cache items being expired, adding an item to my TTLCache is 230.9435502424312 times faster than the core.cache one
With 50 out of 5,000 cache items being expired, adding an item to my TTLCache is 4.757156584034744 times faster than the core.cache one
With 250 out of 5,000 cache items being expired, adding an item to my TTLCache is 0.752112969429968 times faster than the core.cache one
Expand All @@ -65,17 +121,18 @@ My TTLCache: adding an item to a cache with 5,000 entries takes 1.2136739023112

## Running the tests

`lein test uk.me.rkd.ttlcache-test` will run a quick set of unit tests to verify the function.

`lein test uk.me.rkd.core-cache-comparison-test` will run a more exhaustive set of tests based on Criterium to verify that the caches have the expected O(log n) complexity, and to characterise its performance strengths and weaknesses against the core.cache version.

## TODO
`lein test coop.magnet.ttlcache-test` will run a quick set of unit tests
to verify the function.

* Test with [jc-pheap](https://code.google.com/p/jc-pheap/) to see if performance improves over [data.priority-map](https://github.com/clojure/data.priority-map)
`lein test coop.magnet.core-cache-comparison-test` will run a more
exhaustive set of tests based on Criterium to verify that the caches
have the expected O(log n) complexity, and to characterise its
performance strengths and weaknesses against the core.cache version.

## License

Copyright © 2014 Robert Day
Copyright © 2021 Magnet, S. Coop.

Distributed under the Eclipse Public License either version 1.0 or (at
your option) any later version.

0 comments on commit bccdca5

Please sign in to comment.