View Source ActiveMemory.Store (ActiveMemory v0.7.1)

The Store

store-api

Store API

  • Store.all/0 Get all records stored
  • Store.delete/1 Delete the record provided
  • Store.delete_all/0 Delete all records stored
  • Store.one/1 Get one record matching either an attributes search or match query
  • Store.select/1 Get all records matching either an attributes search or match query
  • Store.withdraw/1 Get one record matching either an attributes search or match query, delete the record and return it
  • Store.write/1 Write a record into the memmory table

concurrency

Concurrency

A Store is a GenServer, but the data functions above (all/0, one/1, select/1, write/1, delete/1, withdraw/1, delete_all/0) are not routed through that process and are not serialized by it. They are ordinary module functions that run in the caller's process, delegating straight to the table's adapter (and therefore to :ets/:mnesia). Concurrency is governed by ETS/Mnesia themselves, so many processes read and write in parallel — the single GenServer is not a bottleneck. Only lifecycle and metadata operations (init, state/0, reload_seeds/0) actually use the GenServer.

These functions live on the GenServer module purely for organization: the Store is the single place responsible for how the application talks to its table, following the Single Responsibility Principle. See the S.T.O.N.E principles for the broader design philosophy.

expiry-ttl

Expiry (TTL)

When the Store's Table declares a ttl (see ActiveMemory.Table), records expire automatically. Expiry is enforced in two ways: reads (one/1, select/1, all/0, withdraw/1) never return an expired record, and the Store periodically sweeps expired records to reclaim memory. The sweep cadence defaults to one minute and can be set with the sweep_interval option (milliseconds):

defmodule MyApp.Tokens.Store do
use ActiveMemory.Store,
  table: MyApp.Tokens.Token,
  sweep_interval: :timer.seconds(30)
end

The sweep only runs when the table has a ttl; otherwise it is never scheduled.

seeding

Seeding

When starting a Store there is an option to provide a valid seed file and have the Store auto load seeds contained in the file.

defmodule MyApp.People.Store do
use ActiveMemory.Store,
  table: MyApp.People.Person,
  seed_file: Path.expand("person_seeds.exs", __DIR__)
end

before-init

Before init

All stores are GenServers and have init functions. While those are abstracted you can still specify methods to run during the init phase of the GenServer startup. Use the before_init keyword and add the methods as tuples with the arguments.

defmodule MyApp.People.Store do
use ActiveMemory.Store,
  table: MyApp.People.Person,
  before_init: [{:run_me, ["arg1", "arg2", ...]}, {:run_me_too, []}]
end

before_init and table recovery

For ETS stores, the table is preserved across a store crash/restart by ActiveMemory.TableHeir. On such a recovery seed files are not re-run, but before_init methods always run, including on recovery. If a before_init method writes records with unique or generated keys (for example a uuid), running it again on recovery can create duplicates.

How to handle this is left to the implementer. One option is to make any before_init write follow a "find or create" pattern — check with one/1 before calling write/1 — so the method is idempotent across restarts:

def run_me(args) do
  record = build_record(args)

  case one(%{key: record.key}) do
    {:ok, existing} -> {:ok, existing}
    {:error, :not_found} -> write(record)
  end
end

initial-state

Initial State

All stores are GenServers and thus have a state. The default state is a map as such:

%{started_at: "date time when first started", table_name: MyApp.People.Person}

This default state can be overwritten with a new state structure or values by supplying a method and arguments as a tuple to the keyword initial_state. The method must return {:ok, new_state}.

defmodule MyApp.People.Store do
use ActiveMemory.Store,
  table: MyApp.People.Person,
  initial_state: {:initial_state_method, ["arg1", "arg2", ...]}
end