View Source ActiveMemory.ActiveRepo (ActiveMemory v0.8.2)

The ActiveRepo

An ActiveRepo manages multiple ActiveMemory.Tables from a single process. It is the multi-table counterpart to ActiveMemory.Store (which manages a single table), giving you one supervised entry point and a unified API over many tables.

It is named ActiveRepo rather than Repo so it does not collide with an application's Ecto.Repo while keeping the familiar "repo" terminology.

defmodule MyApp.ActiveRepo do
  use ActiveMemory.ActiveRepo,
    tables: [
      MyApp.People.Person,
      {MyApp.Dogs.Dog, seed_file: Path.expand("dog_seeds.exs", __DIR__), before_init: [{:warm, []}]}
    ]
end

Add the ActiveRepo to your supervision tree like any other process:

children = [MyApp.ActiveRepo]

Tables may freely mix :ets and :mnesia adapters; each operation dispatches to the adapter configured on the given table.

activerepo-api

ActiveRepo API

Every operation an ActiveMemory.Store offers is available here, with the same behavior; only the arities differ. Reads and withdraw take the table module as the first argument, while write and delete infer the table from the struct (or from a changeset's data).

  • ActiveRepo.all/2 Get all records stored in a table, optionally ordered and paged
  • ActiveRepo.count/2 Count the records in a table, without reading them
  • ActiveRepo.delete/1 Delete the record provided, matched in full (see Deleting a record)
  • ActiveRepo.delete_all/1 Delete all records stored in a table
  • ActiveRepo.exists?/3 Whether any record in a table matches an attributes search or match query
  • ActiveRepo.get/2 Get the record with the given primary key, or {:error, :not_found}
  • ActiveRepo.get!/2 Like get/2 but raises ActiveMemory.NotFoundError
  • ActiveRepo.get_by/2 Get the single record in a table matching an attributes search
  • ActiveRepo.get_by!/2 Like get_by/2 but raises ActiveMemory.NotFoundError
  • ActiveRepo.one/2 Get one record from a table matching either an attributes search or match query. Raises ActiveMemory.MultipleResultsError when several match
  • ActiveRepo.one!/2 Like one/2 but raises ActiveMemory.NotFoundError
  • ActiveRepo.reload/1 Re-read a record by its primary key, inferring the table
  • ActiveRepo.reload!/1 Like reload/1 but raises ActiveMemory.NotFoundError
  • ActiveRepo.select/3 Get all records from a table matching either an attributes search or match query, optionally ordered and paged
  • ActiveRepo.withdraw/2 Atomically get one record from a table matching either an attributes search or match query, delete the record and return it — exactly one concurrent caller wins, making it safe for take-once workloads
  • ActiveRepo.write/1 Write a record into its table, from a struct or an Ecto.Changeset. An invalid changeset is returned as {:error, changeset} with its action set to :insert, exactly like Ecto.Repo.insert/2

An operation for a struct or table that is not part of the ActiveRepo returns {:error, :unknown_table}.

reading-counting-ordering

Reading, counting, ordering

These behave exactly as they do on a ActiveMemory.Store, which documents them in full: reading a single record, counting, and ordering with :order_by/:limit/:offset. get/2 reads by the table's primary key (its first field), count/2 is O(1) and takes sweep: true on a ttl table, and reload/1 infers its table from the struct as write/1 and delete/1 do.

deleting-a-record

Deleting a record

delete/1 removes an exact record match: the struct you pass is compared field for field against what is stored. A struct that has diverged from the stored copy — a stale read, or one modified in memory — removes nothing and still returns :ok, the same answer given for a record that was never there. That is the only correct behavior for a :bag table, and on a :set table it keeps a delete from clobbering a newer version of the record.

When you hold an identifier rather than a record you know is current, use withdraw/2: it matches on a query, is atomic, and reports whether anything was removed with {:ok, record} or {:error, :not_found}.

concurrency

Concurrency

Like a Store, an ActiveRepo is a GenServer, but the data functions above are not routed through that process and are not serialized by it. They run in the caller's process and delegate straight to each table's adapter, so reads and writes execute with ETS/Mnesia concurrency — the single GenServer is not a bottleneck. Only lifecycle and metadata operations (init, state/0, reload_seeds/1) use the GenServer.

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

tables-and-per-table-options

Tables and per-table options

Each entry of tables: is either a table module or a {table, opts} tuple. The supported per-table options mirror the single-table ActiveMemory.Store:

  • seed_file a path to a seed file auto loaded when the table is first created
  • before_init methods (defined on the ActiveRepo) run during the table's setup

expiry-ttl

Expiry (TTL)

Any table whose ActiveMemory.Table declares a ttl expires its records automatically: reads never return an expired record, and the ActiveRepo periodically sweeps expired records from every ttl table it owns to reclaim memory. The sweep cadence defaults to one minute and can be set with the sweep_interval option (milliseconds). Tables without a ttl are left untouched, and the sweep is only scheduled when at least one table uses a ttl.

initial-state

Initial State

Like a Store, an ActiveRepo is a GenServer with state. The default state is:

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

Supply a {method, args} tuple to the initial_state keyword to override it; the method must return {:ok, new_state}.

resilience

Resilience

ETS tables created by an ActiveRepo get the same ActiveMemory.TableHeir protection as a Store: they survive an ActiveRepo crash and are reclaimed on restart, and seed files are not re-run on recovery. See ActiveMemory.Store for the before_init recovery caveat, which applies here as well.