A simple, fast and process-free cache. Or, a Dumb Cache.
Overview
The cache is broken into a configurable number of ETS tables (called segments). The segment for a given key is :erlang.phash2(key, NUMBER_OF_SEGMENT) and all operations (get, put, del, ttl, …) are directed to the correct segment.
Expired values are only removed from the cache when get is called on them.
While a maximum cache size must be configured, the size limit is enforced on the segment. That is, given 10 segments and a max size of 10_000, the maximum size of each segment will be 1000. When a segment is full, the segment is erased.
The cache spawns no additional process.
putmay result in an O(N) operation when a segment is (N = MAX_SIZE / NUMBER_OF_SEGMENT). Creating a cache with more segment reduces this cost. Specifically, the O(N) operation is a call to :ets.delete_all_objects/1.
The cache will never grow beyond the configured MAX_SIZE.
Your keys might hash to only 1 or a few segments. This would negatively impact performance. However, this would likely require a very unfortunate set of keys (e.g. integers with a fixed gaps between them).
Usage 1
The preferred usage, which offers better performance, is to use the define/3 macro:
defmodule MyApp.Cache do
require DCache
DCache.define(Users, 100_000)
DCache.define(Products, 1_000_000, segment: 100)
endAnd then call the following on application start:
MyApp.Cache.Users.setup()
MyApp.Cache.Products.setup()You can then use the caching methods on the created module, e.g:
MyApp.Cache.Users.get(KEY)Usage 2
Alternatively, you can create the cache at runtime. Note that this will require 1 extra ETS lookup for every call, as well as requiring 1 extra ETS table to be allocated.
DCache.setup(:users, 100_000)
DCache.setup(:products, 1_000_000, segments: 100)Which you can then use as:
DCache.get(:users, KEY)Functions
Regardless of which of the above two approaches you use, the same functionality is available:
get/1 & get/2
Cache.Users.get(key)
# OR
DCache.get(:users, key)
Returns the value from the cache. Returns nil if the key isn’t found or if the value is expired.
del/1 & del/2
Cache.Users.del(key)
# OR
DCache.del(:users, key)Deletes the value from the cache. Does nothing if the key isn’t found. The return value is currently always true.
ttl/1 & ttl/2
Cache.Users.ttl(key)
# OR
DCache.ttl(:users, key)
Returns the unix time, in seconds, when the value will be considered expired. Returns nil if the key isn’t found. This can return a value which is less than now.
put/3 & put/4
Cache.Users.put(key, value, ttl)
# OR
DCache.put(:users, key, value, ttl)Stores the value in the cache. The ttl is given in seconds relative to now (e.g. 300 to have the value expire in 5 minutes).
fetch/2, fetch/3 & fetch/4
Cache.Users.fetch(key, fun)
# OR
DCache.fetch(:users, key, fun)
# OR
Cache.Users.fetch(key, fun, ttl)
# OR
DCache.fetch(:users, key, fun, ttl)Returns the value from the cache. If the key is not found, or expired, fun/1 will be called (receiving the key as an argument).
The provided fun/1 should return one of:
{:ok, value}- to putvalueinto the cache with thettlthat was provided tofetch.{:ok, value}will be returned fromfetch.{:ok, value, ttl}- to putvalueinto the cache with the specifiedttl.{:ok, value}will be returned fromfetch.{:skip, value} - does not placevaluein the cache, but still returnsvaluefromfetch{:error, anything}- does not place value in the cache and return{:error, anything}from fetch.
fetch!/2, fetch!/3 & fetch!/4
Same as fetch/2, fetch/3 & fetch/4, but unwraps {:ok, value} into value and {:error, err} into raise err.
size/0 & size/1
Cache.Users.size()
# OR
DCache.size(:users)Returns the total number of items in the cache, including expired items. This is an O(N) operation where N is the number of segments (which will typically be small).