ConCache (con_cache v1.1.0) View Source

Implements an ETS based key/value storage with following additional features:

  • row level synchronized writes (inserts, read/modify/write updates, deletes)
  • TTL support
  • modification callbacks

Example usage:

ConCache.start_link(name: :my_cache, ttl_check_interval: false)
ConCache.put(:my_cache, :foo, 1)
ConCache.get(:my_cache, :foo)  # 1

The following rules apply:

  • Modifications are by isolated per row. Two processes can't modify the same row at the same time. Dirty operations are available through dirty_ equivalents.
  • Reads are dirty by default. You can use isolated/4 to perform isolated custom operations.
  • Operations are always performed in the caller process. Custom lock implementation is used to ensure synchronism. See README.md for more details.
  • In this example, items don't expire. See start_link/1 for details on how to setup expiry.

See start_link/1 for more details.

Link to this section Summary

Functions

Deletes the item from the cache.

Dirty equivalent of delete/2.

Dirty equivalent of put/3.

Returns the ets table managed by the cache.

Retrieves the item from the cache, or inserts the new item.

Reads the item from the cache.

Retrieves the item from the cache, or inserts the new item.

Inserts the item into the cache unless it exists.

Isolated execution over arbitrary lock in the cache.

Stores the item into the cache.

Returns the number of items stored in the cache.

Starts the server and creates an ETS table.

Manually touches the item to prolongate its expiry.

Similar to isolated/4 except it doesn't wait for the lock to be available.

Updates the item, or stores new item if it doesn't exist.

Updates the item only if it exists. Otherwise works just like update/3.

Link to this section Types

Specs

callback_fun() ::
  ({:update, pid(), key(), value()} | {:delete, pid(), key()} -> any())

Specs

ets_option() ::
  :named_table
  | :compressed
  | {:heir, pid()}
  | {:write_concurrency, boolean()}
  | {:read_concurrency, boolean()}
  | {:decentralized_counters, boolean()}
  | :ordered_set
  | :set
  | :bag
  | :duplicate_bag
  | {:name, atom()}

Specs

fetch_or_store_fun() :: (-> {:ok, store_value()} | {:error, any()})

Specs

key() :: any()

Specs

options() :: [
  name: atom(),
  global_ttl: non_neg_integer(),
  acquire_lock_timeout: pos_integer(),
  callback: callback_fun(),
  touch_on_read: boolean(),
  ttl_check_interval: non_neg_integer() | false,
  time_size: pos_integer(),
  ets_options: [ets_option()],
  n_lock_partitions: pos_integer()
]

Specs

store_fun() :: (-> store_value())

Specs

store_value() :: value() | ConCache.Item.t()

Specs

t() :: pid() | atom() | {:global, any()} | {:via, atom(), any()}

Specs

update_fun() :: (value() -> {:ok, store_value()} | {:error, any()})

Specs

value() :: any()

Link to this section Functions

Specs

delete(t(), key()) :: :ok

Deletes the item from the cache.

Link to this function

dirty_delete(cache_id, key)

View Source

Specs

dirty_delete(t(), key()) :: :ok

Dirty equivalent of delete/2.

Link to this function

dirty_fetch_or_store(cache_id, key, fetch_or_store_fun)

View Source

Specs

dirty_fetch_or_store(t(), key(), fetch_or_store_fun()) ::
  {:ok, value()} | {:error, any()}

Dirty equivalent of fetch_or_store/3.

Link to this function

dirty_get_or_store(cache_id, key, store_fun)

View Source

Specs

dirty_get_or_store(t(), key(), store_fun()) :: value()

Dirty equivalent of get_or_store/3.

Link to this function

dirty_insert_new(cache_id, key, value)

View Source

Specs

dirty_insert_new(t(), key(), store_value()) :: :ok | {:error, :already_exists}

Dirty equivalent of insert_new/3.

Link to this function

dirty_put(cache_id, key, value)

View Source

Specs

dirty_put(t(), key(), store_value()) :: :ok

Dirty equivalent of put/3.

Link to this function

dirty_update(cache_id, key, update_fun)

View Source

Specs

dirty_update(t(), key(), update_fun()) :: :ok | {:error, any()}

Dirty equivalent of update/3.

Link to this function

dirty_update_existing(cache_id, key, update_fun)

View Source

Specs

dirty_update_existing(t(), key(), update_fun()) ::
  :ok | {:error, :not_existing} | {:error, any()}

Dirty equivalent of update_existing/3.

Specs

ets(t()) :: :ets.tab()

Returns the ets table managed by the cache.

Link to this function

fetch_or_store(cache_id, key, fetch_or_store_fun)

View Source

Specs

fetch_or_store(t(), key(), fetch_or_store_fun()) ::
  {:ok, value()} | {:error, any()}

Retrieves the item from the cache, or inserts the new item.

If the item exists in the cache, it is retrieved. Otherwise, the lambda function is executed and its result is stored under the given key, but only if it returns an {:ok, value} tuple. If the {:error, reason} tuple is returned, caching is not done and the error becomes the result of the function. If the lambda returns none of the above, a RuntimeError is raised.

The lambda may return either a plain value or %ConCache.Item{}.

This function is not supported by :bag and :duplicate_bag ETS tables.

Note: if the item is already in the cache, this function amounts to a simple get without any locking, so you can expect it to be fairly fast.

Specs

get(t(), key()) :: value()

Reads the item from the cache.

A read is always "dirty", meaning it doesn't block while someone is updating the item under the same key. A read doesn't expire TTL of the item, unless touch_on_read option is set while starting the cache.

Link to this function

get_or_store(cache_id, key, store_fun)

View Source

Specs

get_or_store(t(), key(), store_fun()) :: value()

Retrieves the item from the cache, or inserts the new item.

If the item exists in the cache, it is retrieved. Otherwise, the lambda function is executed and its result is stored under the given key.

The lambda may return either a plain value or %ConCache.Item{}.

This function is not supported by :bag and :duplicate_bag ETS tables.

Note: if the item is already in the cache, this function amounts to a simple get without any locking, so you can expect it to be fairly fast.

Link to this function

insert_new(cache_id, key, value)

View Source

Specs

insert_new(t(), key(), store_value()) :: :ok | {:error, :already_exists}

Inserts the item into the cache unless it exists.

Link to this function

isolated(cache_id, key, timeout \\ nil, fun)

View Source

Specs

isolated(t(), key(), nil | pos_integer(), (-> any())) :: any()

Isolated execution over arbitrary lock in the cache.

You can do whatever you want in the function, not necessarily related to the cache. The return value is the result of the provided lambda.

This allows you to perform flexible isolation. If you use the key of your item as a key, then this operation will be exclusive to updates. This can be used e.g. to perform isolated reads:

# Process A:
ConCache.isolated(:my_cache, :my_item_key, fn() -> ... end)

# Process B:
ConCache.update(:my_cache, :my_item, fn(old_value) -> ... end)

These two operations are mutually exclusive.

Link to this function

put(cache_id, key, value)

View Source

Specs

put(t(), key(), store_value()) :: :ok

Stores the item into the cache.

Specs

size(t()) :: non_neg_integer()

Returns the number of items stored in the cache.

Specs

start_link(options()) :: Supervisor.on_start()

Starts the server and creates an ETS table.

Options:

  • {:name, atom} - A name of the cache process.
  • {:ttl_check_interval, time_ms | false} - Required. A check interval for TTL expiry. Provide a positive integer for expiry to work, or pass false to disable ttl checks. See below for more details on expiry.
  • {:global_ttl, time_ms | :infinity} - The time after which an item expires. When an item expires, it is removed from the cache. Updating the item extends its expiry time.
  • {:touch_on_read, true | false} - Controls whether read operation extends expiry of items. False by default.
  • {:callback, callback_fun} - If provided, this function is invoked after an item is inserted or updated, or before it is deleted.
  • {:acquire_lock_timeout, timeout_ms} - The time a client process waits for the lock. Default is 5000.
  • {:ets_options, [ets_option] – The options for ETS process.
  • {:n_lock_partitions, pos_integer} - A positive integer representing the desired number of lock partitions

In addition, following ETS options are supported:

  • :set - An ETS table will be of the :set type (default).
  • :ordered_set - An ETS table will be of the :ordered_set type.
  • :bag - An ETS table will be of the :bag type.
  • :duplicate_bag - An ETS table will be of the :duplicate_bag type.
  • :named_table
  • :name
  • :heir
  • :write_concurrency
  • :read_concurrency
  • :decentralized_counters

Child specification

To insert your cache into the supervision tree, pass the child specification in the shape of {ConCache, con_cache_options}. For example:

{ConCache, [name: :my_cache, ttl_check_interval: false]}

Expiry

To configure expiry, you need to provide positive integer for the :ttl_check_interval option. This integer represents the millisecond interval in which the expiry is performed. You also need to provide the :global_ttl option, which represents the default TTL time for the item.

TTL of each item is by default extended only on modifications. This can be changed with the touch_on_read: true option.

If you need a granular control of expiry per each item, you can pass a ConCache.Item struct when storing data.

If you don't want a modification of an item to extend its TTL, you can pass a ConCache.Item struct, with :ttl field set to :no_update.

Choosing ttl_check_interval time

When expiry is configured, the owner process works in discrete steps, doing cleanups every ttl_check_interval milliseconds. This approach allows the owner process to do fairly small amount of work in each discrete step.

Assuming there's no huge system overload, an item's max lifetime is thus global_ttl + ttl_check_interval [ms], after the last item's update.

Thus, a lower value of ttl_check_interval time means more frequent purging which may reduce your memory consumption, but could also cause performance penalties. Higher values put less pressure on processing, but item expiry is less precise.

Specs

touch(t(), key()) :: :ok

Manually touches the item to prolongate its expiry.

Link to this function

try_isolated(cache_id, key, timeout \\ nil, on_success)

View Source

Specs

try_isolated(t(), key(), nil | pos_integer(), (-> any())) ::
  {:error, :locked} | {:ok, any()}

Similar to isolated/4 except it doesn't wait for the lock to be available.

If the lock can be acquired immediately, it will be acquired and the function will be invoked. Otherwise, an error is returned immediately.

Link to this function

update(cache_id, key, update_fun)

View Source

Specs

update(t(), key(), update_fun()) :: :ok | {:error, any()}

Updates the item, or stores new item if it doesn't exist.

The update_fun is invoked after the item is locked. Here, you can be certain that no other process will update this item, unless they are doing dirty updates or writing directly to the underlying ETS table. This function is not supported by :bag or :duplicate_bag ETS tables.

The updater lambda must return one of the following:

  • {:ok, value} - causes the value to be stored into the table
  • {:error, reason} - the value won't be stored and {:error, reason} will be returned
Link to this function

update_existing(cache_id, key, update_fun)

View Source

Specs

update_existing(t(), key(), update_fun()) ::
  :ok | {:error, :not_existing} | {:error, any()}

Updates the item only if it exists. Otherwise works just like update/3.