Hi all
— a big update since I first announced ActiveMemory here.
v0.7 is now on Hex, and it’s grown from a simple ORM into a typed, attribute-queryable in-memory store for ETS and Mnesia — with the safety rails you’d want before trusting it with short-lived or sensitive data. Four highlights:
Atomic, take-once withdraw/1
withdraw/1 now fetches-and-deletes in a single atomic operation — :ets.select_delete/2 for ETS, a :mnesia.transaction/1 for Mnesia. Under concurrent access exactly one caller gets a given record; everyone else gets {:error, :not_found}. Great for one-time tokens and job-claim patterns:
{:ok, token} = TokenStore.withdraw(%{value: submitted_token})
TTL / record expiry
Give a table a ttl and its records expire automatically:
defmodule MyApp.Tokens.Token do
use ActiveMemory.Table, type: :ets, ttl: :timer.hours(1)
attributes do
field(:value)
field(:user_id)
end
end
Enforcement is hybrid: a lazy read-filter means one/select/all/withdraw never return an expired record (exact and immediate), and a periodic sweep deletes them to reclaim memory (sweep_interval configurable, default 60s). Zero overhead for tables without a ttl.
Crash resilience (TableHeir)
An ETS table is owned by the process that creates it, so a Store crash used to mean losing the data. ActiveMemory now runs a tiny, stable heir process registered as the ETS :heir; on a crash the table transfers to it, and the restarted Store reclaims it — data intact. No configuration, no API change. (Mnesia tables were never affected — they already survive process crashes.)
ActiveRepo — multiple tables, one process
For several tables under one supervised entry point:
defmodule MyApp.Repo do
use ActiveMemory.ActiveRepo,
tables: [MyApp.People.Person, MyApp.Tokens.Token]
end
MyApp.Repo.write(%Person{first: "Kara"}) # table inferred from the struct
MyApp.Repo.one(Token, %{value: raw}) # reads take the table explicitly
It’s named ActiveRepo (not Repo) so it won’t collide with your Ecto.Repo, and a single repo can mix ETS and Mnesia tables — each with its own ttl if you like.
Where it fits
ActiveMemory isn’t a cache — if you want key-based caching with eviction and hit/miss stats, Cachex and Nebulex are excellent. ActiveMemory is for structured records in memory that you query by their attributes:
SessionStore.select(%{user_id: user_id, active?: true})
StaffStore.select(match(:role == "admin" and :last_login < cutoff))
…the kind of query a key/value store can’t express without hand-rolled secondary indexes.
A note on concurrency
A question that came up at a conference recently: “isn’t the single GenServer a bottleneck?” No — the CRUD functions run in the caller’s process and delegate straight to the adapter, so reads/writes get full ETS/Mnesia concurrency. The GenServer only handles lifecycle, state, and the TTL sweep.
Docs: hexdocs.pm/active_memory · Repo: github.com/SullysMustyRuby/active_memory
Would love feedback, use cases, or contributions — and if anything in the new features doesn’t behave as documented, please open an issue. Thanks!