XUtils

cache

In-memory Segmented Cache


Cache

Library implements segmented in-memory cache.

Build Status Coverage Status Hex.pm Hex Downloads

Inspiration

Cache uses N disposable ETS tables instead of single one. The cache applies eviction and quota policies at segment level. The oldest ETS table is destroyed and new one is created when quota or TTL criteria are exceeded. This approach outperforms the traditional timestamp indexing techniques.

The write operation always uses youngest segment. The read operation lookup key from youngest to oldest table until it is found same time key is moved to youngest segment to prolong TTL. If none of ETS table contains key then cache-miss occurs.

The downside is inability to assign precise TTL per single cache entry. TTL is always approximated to nearest segment. (e.g. cache with 60 sec TTL and 10 segments has 6 sec accuracy on TTL)

Getting started

The latest version of the library is available at its master branch. All development, including new features and bug fixes, take place on the master branch using forking and pull requests as described in contribution guidelines.

The stable library release is available via hex packages, add the library as dependency to rebar.config

{deps, [
   cache
]}.

key/value interface

The library implements traditional key/value interface through put, get and remove functions. The function get prolongs ttl of the item, use lookup to keep ttl untouched.

application:start(cache).
{ok, _} = cache:start_link(my_cache, [{n, 10}, {ttl, 60}]).

ok  = cache:put(my_cache, <<"my key">>, <<"my value">>).
Val = cache:get(my_cache, <<"my key">>).

asynchronous i/o

The library provides synchronous and asynchronous implementation of same functions. The asynchronous variant of function is annotated with _ suffix. E.g. get(...) is a synchronous cache lookup operation (the process is blocked until cache returns); get_(...) is an asynchronous variant that delivers result of execution to mailbox.

application:start(cache).
{ok, _} = cache:start_link(my_cache, [{n, 10}, {ttl, 60}]).

Ref = cache:get_(my_cache, <<"my key">>).
receive {Ref, Val} -> Val end.

transform element

The library allows to read-and-modify (modify in-place) cached element. You can apply any function over cached elements and returns the result of the function. The apply acts a transformer with three possible outcomes:

  • undefined (e.g. fun(_) -> undefined end) - no action is taken, old cache value remains;
  • unchanged value (e.g. fun(X) -> X end) - no action is taken, old cache value remains;
  • new value (e.g. fun(X) -> <<"x", X/binary>> end) - the value in cache is replaced with the result of the function.
application:start(cache).
{ok, _} = cache:start_link(my_cache, [{n, 10}, {ttl, 60}]).

cache:put(my_cache, <<"my key">>, <<"x">>).
cache:apply(my_cache, <<"my key">>, fun(X) -> <<"x", X/binary>> end).
cache:get(my_cache, <<"my key">>).

The library implement helper functions to transform elements with append or prepend.

application:start(cache).
{ok, _} = cache:start_link(my_cache, [{n, 10}, {ttl, 60}]).

cache:put(my_cache, <<"my key">>, <<"b">>).
cache:append(my_cache, <<"my key">>, <<"c">>).
cache:prepend(my_cache, <<"my key">>, <<"a">>).
cache:get(my_cache, <<"my key">>).

accumulator

application:start(cache).
{ok, _} = cache:start_link(my_cache, [{n, 10}, {ttl, 60}]).

cache:acc(my_cache, <<"my key">>, 1).
cache:acc(my_cache, <<"my key">>, 1).
cache:acc(my_cache, <<"my key">>, 1).

check-and-store

The library implements the check-and-store semantic for put operations:

  • add store key/val only if cache does not already hold data for this key
  • replace store key/val only if cache does hold data for this key

configuration via Erlang sys.config

The cache instances are configurable via sys.config. These cache instances are supervised by application supervisor.

{cache, [
   {my_cache, [{n, 10}, {ttl, 60}]}
]}

distributed environment

The cache application uses standard Erlang distribution model. Please node that Erlang distribution uses single tcp/ip connection for message passing between nodes. Therefore, frequent read/write of large entries might impact on overall Erlang performance.

The global cache instance is visible to all Erlang nodes in the cluster.

%% at a@example.com
{ok, _} = cache:start_link({global, my_cache}, [{n, 10}, {ttl, 60}]).
Val = cache:get({global, my_cache}, <<"my key">>).

%% at b@example.com
ok  = cache:put({global, my_cache}, <<"my key">>, <<"my value">>).
Val = cache:get({global, my_cache}, <<"my key">>).

The local cache instance is accessible for any Erlang nodes in the cluster.

%% a@example.com
{ok, _} = cache:start_link(my_cache, [{n, 10}, {ttl, 60}]).
Val = cache:get(my_cache, <<"my key">>).

%% b@example.com
ok  = cache:put({my_cache, 'a@example.com'}, <<"my key">>, <<"my value">>).
Val = cache:get({my_cache, 'a@example.com'}, <<"my key">>).

sharding

Module cache_shards provides simple sharding on top of cache. It uses simple hash(Key) rem NumShards approach, and keeps NumShards in application environment. This feature is still experimental, its interface is a subject to change in further releases.

{ok, _} = cache_shards:start_link(my_cache, 8, [{n, 10}, {ttl, 60}]).
ok = cache_shards:put(my_cache, key1, "Hello").
{ok,"Hello"} = cache_shards:get(my_cache, key1).

sharded_cache uses only small subset of cache API. But you can get shard name for your key and then use cache directly.

{ok, Shard} = cache_shards:get_shard(my_cache, key1)
{ok, my_cache_2}
cache:lookup(Shard, key1).
"Hello"

Bugs

If you detect a bug, please bring it to our attention via GitHub issues. Please make your report detailed and accurate so that we can identify and replicate the issues you experience:

  • specify the configuration of your environment, including which operating system you’re using and the versions of your runtime environments
  • attach logs, screen shots and/or exceptions if possible
  • briefly summarize the steps you took to resolve or reproduce the problem

Changelog

  • 2.3.0 - sharding of cache bucket (single node only)
  • 2.0.0 - various changes on asynchronous api, not compatible with version 1.x
  • 1.0.1 - production release

Contributors


Articles

  • coming soon...