Current section

Files

Jump to
active_memory lib active_memory.ex
Raw

lib/active_memory.ex

defmodule ActiveMemory do
  @moduledoc """
  The typed, attribute-queryable in memory store for ETS and Mnesia.

  In most applications, a huge share of database load is reads of data that barely
  changes: reference data, configuration, authorization, catalog data. ActiveMemory
  exists to take that load off the database — load those tables into memory once at
  boot, serve every read *for those tables* from RAM, and write back only when
  something actually changes. A cache with no cache to manage: no keys to design,
  no cold misses, no invalidation dance. The in-memory copy serves the reads, while
  the database remains the durable source of truth.

  Records are found by their **attributes**, in any combination, with no ETS match
  specifications to hand write:

  ```elixir
  AdminStore.one(%{email: email, active?: true})
  ProductStore.select(%{category: "electronics", in_stock?: true})
  TokenStore.withdraw(%{value: submitted_token})
  ```

  The same interface runs on `:ets` or `:mnesia`, with boot-time seeding, record
  expiry, crash resilience and atomic take-once reads built in.

  ## The pieces

    - `ActiveMemory.Table` — define a table's fields and its ETS/Mnesia options. A
      table is either an `attributes` block or an Ecto `embedded_schema`.
    - `ActiveMemory.Store` — a supervised process owning **one** table, and the API
      you read and write through.
    - `ActiveMemory.ActiveRepo` — the same API over **several** tables from one
      process.
    - `ActiveMemory.Query` — the `match/1` macro, for queries that need more than
      equality.

  ## Getting started

  Define a table, define a store, add the store to your supervision tree.

  ```elixir
  defmodule MyApp.People.Person do
    use ActiveMemory.Table, options: [index: [:last]]

    attributes auto_generate_uuid: true do
      field(:email, :string)
      field(:first, :string)
      field(:last, :string)
      field(:age, :integer)
      field(:admin?, :boolean)
    end
  end

  defmodule MyApp.People.Store do
    use ActiveMemory.Store, table: MyApp.People.Person
  end
  ```

  ```elixir
  # in MyApp.Application
  children = [MyApp.People.Store]
  ```

  That is the whole setup. The table is created when the store starts, so there are
  no migrations to run.

  ```elixir
  {:ok, person} = MyApp.People.Store.write(%MyApp.People.Person{email: "kara@bsg.com"})
  {:ok, person} = MyApp.People.Store.get(person.uuid)
  people = MyApp.People.Store.all(order_by: :last, limit: 20)
  ```

  Tables default to `:mnesia`; pass `type: :ets` for an ETS table. See
  `ActiveMemory.Store` for the full read and write API.

  ## Working with Ecto

  A table takes Ecto types, and can be an Ecto schema outright, so `Ecto.Changeset`
  works on it and `write/1` accepts a changeset the way `c:Ecto.Repo.insert/2` does:

  ```elixir
  %MyApp.People.Person{}
  |> MyApp.People.Person.changeset(params)
  |> MyApp.People.Store.write()
  ```

  The [Coming from Ecto](coming_from_ecto.html) guide covers what carries over, what
  is named differently, and where the two genuinely differ.

  ## What else is built in

    - **Record expiry.** A `ttl` on a table gives every record a lifetime; reads never
      return an expired record and the owning process sweeps them to reclaim memory.
      See `ActiveMemory.Table`.
    - **Crash resilience.** `ActiveMemory.TableHeir` holds ETS tables when a store
      crashes, so the data survives the restart. No configuration needed.
    - **Atomic take-once reads.** `withdraw/1` finds a record and removes it in one
      atomic operation, so exactly one concurrent caller wins — what you want for one
      time tokens and 2FA codes.

  ## Testing

  A table's module name is its ETS/Mnesia table name, so isolation comes down to
  whether tests share a table. A test module that defines its own table and store
  runs `async: true` alongside every other test module with no configuration. Test
  modules that share your application's store need `async: false`, since they write
  to the same global table. See [Testing](readme.html#testing) for both patterns.

  ## When to reach for it

  ActiveMemory suits a small-to-medium dataset that is read constantly but changes
  rarely — reference data, configuration, admin users and permissions, products and
  plans — plus short-lived records that benefit from `ttl` and `withdraw/1`, like
  one time tokens and 2FA codes.

  It is **not** a system of record. ETS lives and dies with the node, and Mnesia
  persists only with `disc_copies`; keep durable data in a database. For caching
  computed values by key with eviction policies, a cache such as
  [Cachex](https://github.com/whitfin/cachex) or
  [Nebulex](https://github.com/elixir-nebulex/nebulex) is the better fit.

  Running across several nodes means a replicated Mnesia table, which makes network
  partitions a concern — see the `majority` option in `ActiveMemory.Table`.
  """
end