SuperCache.Storage (SuperCache v1.3.0)

Copy Markdown View Source

Thin wrapper around :ets that provides the read/write/delete primitives used throughout SuperCache.

All functions accept either an atom (named ETS table) or an :ets.tid() (anonymous table reference) as the partition argument.

This module is intentionally low-level. Application code should go through the higher-level SuperCache, SuperCache.KeyValue, SuperCache.Queue, etc., not through this module directly.

Key position

The ETS keypos is set from :key_pos config during table creation (EtsHolder.create_table/1). The keypos is 1-based in ETS, so a :key_pos of 0 corresponds to keypos: 1.

Concurrency

All tables are created with {:write_concurrency, true} and {:read_concurrency, true}. Multiple processes can read and write the same partition concurrently without external locking, with the exception of structural mutations in Queue and Stack which use a soft application-level lock (see those modules).

Example

alias SuperCache.Storage

# Assuming a table named :my_table already exists:
Storage.put({:user, 1, "Alice"}, :my_table)
Storage.get(:user, :my_table)
# => [{:user, 1, "Alice"}]

Storage.delete(:user, :my_table)
Storage.get(:user, :my_table)
# => []

Summary

Functions

Delete the record at key.

Delete all records in partition.

Delete all records matching pattern using :ets.match_delete/2.

Look up all records with key in partition.

Insert term only if no record with the same key exists.

Insert one or more tuples into partition.

Fold over all records in partition using :ets.foldl/3.

Create num ETS partitions, named <prefix>_0 through <prefix>_{num-1}.

Return {partition, record_count} for partition.

Delete all num ETS partitions.

Atomically remove and return the record at key.

Atomically increment or decrement a counter field.

Update one or more fields in an existing record at key.

Functions

delete(key, partition)

@spec delete(any(), atom() | :ets.tid()) :: true

Delete the record at key.

Always succeeds (no-op when the key does not exist).

Examples

Storage.delete(:session_tok, :my_table)
# => true

delete_all(partition)

@spec delete_all(atom() | :ets.tid()) :: true

Delete all records in partition.

Examples

Storage.delete_all(:my_table)
# => true

delete_match(pattern, partition)

@spec delete_match(atom() | tuple(), atom() | :ets.tid()) :: true

Delete all records matching pattern using :ets.match_delete/2.

Pattern semantics are the same as get_by_match/2.

Examples

# Remove all expired sessions
Storage.delete_match({:session, :_, :expired}, :my_table)
# => true

get(key, partition)

@spec get(any(), atom() | :ets.tid()) :: [tuple()]

Look up all records with key in partition.

Returns a list of tuples (empty when the key does not exist).

Examples

Storage.get(:user, :my_table)
# => [{:user, 1, "Alice"}]

Storage.get(:missing, :my_table)
# => []

get_by_match(pattern, partition)

@spec get_by_match(atom() | tuple(), atom() | :ets.tid()) :: [[any()]]

Pattern match using :ets.match/2.

Returns a list of binding lists. Wildcards are :_; captures are :"$1", :"$2", etc.

Examples

Storage.get_by_match({:user, :"$1", :admin}, :my_table)
# => [[1], [42]]  (ids of admin users)

Storage.get_by_match({:session, :_, :expired}, :my_table)
# => []  (no expired sessions)

get_by_match_object(pattern, partition)

@spec get_by_match_object(atom() | tuple(), atom() | :ets.tid()) :: [tuple()]

Pattern match using :ets.match_object/2.

Returns full matching tuples rather than capture binding lists.

Examples

Storage.get_by_match_object({:user, :_, :admin}, :my_table)
# => [{:user, 1, :admin}, {:user, 42, :admin}]

Storage.get_by_match_object({:user, :_, :banned}, :my_table)
# => []

insert_new(term, partition)

@spec insert_new(tuple(), atom() | :ets.tid()) :: boolean()

Insert term only if no record with the same key exists.

Returns true on success, false if the key is already present. Only meaningful for :set / :ordered_set tables.

Used by Queue and Stack as a compare-and-swap for initialisation.

put(term, partition)

@spec put([tuple()] | tuple(), atom() | :ets.tid()) :: true

Insert one or more tuples into partition.

For :set / :ordered_set tables, inserting a record whose key already exists overwrites the previous record.

Examples

Storage.put({:session, "tok-1", :active}, :my_table)
Storage.put([{:a, 1}, {:b, 2}], :my_table)   # batch insert

scan(fun, acc, partition)

@spec scan((any(), any() -> any()), any(), atom() | :ets.tid()) :: any()

Fold over all records in partition using :ets.foldl/3.

fun/2 receives (record, accumulator) and must return the new accumulator.

Examples

Storage.scan(fn {_, score}, acc -> acc + score end, 0, :my_table)
# => 1024  (sum of all scores)

Storage.scan(fn _rec, acc -> acc + 1 end, 0, :my_table)
# => 50  (count of records)

start(num)

@spec start(pos_integer()) :: :ok

Create num ETS partitions, named <prefix>_0 through <prefix>_{num-1}.

Called by Bootstrap.start!/1 during system startup.

Examples

SuperCache.Storage.start(4)
# => :ok

stats(partition)

@spec stats(atom() | :ets.tid()) ::
  {atom() | :ets.tid(), non_neg_integer() | :undefined}

Return {partition, record_count} for partition.

Uses :ets.info(partition, :size) which is an O(1) operation thanks to the :decentralized_counters option set during table creation.

Examples

Storage.stats(:my_table)
# => {:my_table, 1024}

Storage.stats(:nonexistent)
# => {:nonexistent, :undefined}

stop(num)

@spec stop(pos_integer()) :: :ok

Delete all num ETS partitions.

Called by Bootstrap.stop/0 during system shutdown.

Examples

SuperCache.Storage.stop(4)
# => :ok

take(key, partition)

@spec take(any(), atom() | :ets.tid()) :: [tuple()]

Atomically remove and return the record at key.

Returns [tuple] (empty when the key does not exist). The removal and the return are a single atomic ETS operation — no other process can observe the record between the read and the delete.

Used by Queue and Stack for lock-free counter management.

Examples

Storage.take(:session_tok, :my_table)
# => [{:session_tok, "active"}]

Storage.take(:missing, :my_table)
# => []

update_counter(key, partition, counter_spec)

update_counter(key, partition, counter_spec, default)

Atomically increment or decrement a counter field.

counter_spec follows the :ets.update_counter/3 convention. default is inserted when the key does not yet exist (four-argument form).

update_element(key, partition, element_spec)

update_element(key, partition, element_spec, default)

Update one or more fields in an existing record at key.

element_spec follows the :ets.update_element/3 convention: {position, new_value} or [{position, new_value}, …].

default is inserted as a new record when the key does not yet exist (requires the four-argument form).