table = :ets.new(:cache, [:set, :public, read_concurrency: true])
ETS (Erlang Term Storage) is an in-memory table built into the VM. Any process can read it without sending a message, which makes it the standard tool for hot, shared, read-heavy data like caches.
table = :ets.new(:cache, [:set, :public, read_concurrency: true])
Rows are tuples; the first element is the key.
:ets.insert(table, {"ada", :admin})
:ets.insert(table, {"grace", :editor})
:ets.insert(table, {"ada", :owner})
:set keeps one row per key, so the second insert
for "ada" replaced the first.
IO.inspect(:ets.lookup(table, "ada"), label: "lookup ada")
IO.inspect(:ets.lookup(table, "joe"), label: "lookup joe")
lookup ada: [{"ada", :owner}]
lookup joe: []
Counters are atomic, even with many writers.
:ets.insert(table, {:hits, 0})
1..100
|> Enum.map(fn _ -> Task.async(fn -> :ets.update_counter(table, :hits, 1) end) end)
|> Task.await_many()
IO.inspect(:ets.lookup_element(table, :hits, 2), label: "hits")
hits: 100
:ordered_set keeps keys sorted, so you can walk
ranges. :bag allows several rows per key.
scores = :ets.new(:scores, [:ordered_set])
for {name, score} <- [{"c", 30}, {"a", 10}, {"b", 20}], do: :ets.insert(scores, {name, score})
IO.inspect(:ets.tab2list(scores), label: "ordered_set")
tags = :ets.new(:tags, [:bag])
:ets.insert(tags, [{"post1", "elixir"}, {"post1", "otp"}, {"post2", "elixir"}])
IO.inspect(:ets.lookup(tags, "post1"), label: "bag")
ordered_set: [{"a", 10}, {"b", 20}, {"c", 30}]
bag: [{"post1", "elixir"}, {"post1", "otp"}]
match_object and select query by pattern. :_
matches anything. Select uses match specs; build
them with :ets.fun2ms/1 in IEx when they get hairy.
IO.inspect(:ets.match_object(tags, {:_, "elixir"}), label: "tagged elixir")
high = :ets.select(scores, [{{:"$1", :"$2"}, [{:>, :"$2", 15}], [:"$1"]}])
IO.inspect(high, label: "score > 15")
tagged elixir: [{"post2", "elixir"}, {"post1", "elixir"}]
score > 15: ["b", "c"]
Every table has an owner: the process that created it. When the owner dies, the table is deleted, whatever other processes think.
parent = self()
owner =
spawn(fn ->
t = :ets.new(:short_lived, [:public])
:ets.insert(t, {"answer", 42})
send(parent, {:table, t})
receive do: (:stop -> :ok)
end)
t =
receive do
{:table, t} -> t
end
IO.inspect(:ets.lookup(t, "answer"), label: "while owner lives")
ref = Process.monitor(owner)
send(owner, :stop)
receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
IO.inspect(:ets.info(t), label: "after owner died")
while owner lives: [{"answer", 42}]
after owner died: :undefined
Pitfall: creating an ETS table from a short-lived process (a Task, a request handler) means it vanishes when that process ends. Create long-lived tables in a supervised GenServer that lives as long as the data should.
Pitfall: ETS is not a transaction store.
lookuptheninsertfrom two processes is a race. Useupdate_counter,insert_new, or route writes through one GenServer and let readers hit the table.