I am just learning about ETS
and GenServer
and I am trying to initialize a cache when my app starts. It's quite possible that I am designing this incorrectly which is leading to the issue I describe below, so any feedback on that would be helpful.
When the app initializes, the :ets
table is created via a worker
.
def start_link do
GenServer.start_link(__MODULE__, :ok)
end
def init(:ok) do
tab = :ets.new(:my_table, [:set, :named_table])
:ets.insert(:my_table, {1, "one"})
{:ok, tab}
end
def lookup(key) do
:ets.lookup(:my_table, key)
end
iex(1)> MyApp.DataTable.lookup(1)
[{1, "one"}]
So far so good...but now I want to update that table. So I add a call
:
def add do
GenServer.call(self(), :add)
end
def handle_call(:add, _from, tab) do
tab = :ets.insert(:my_table, {2, "two"})
{:reply, lookup(2), tab}
end
iex(1)> MyApp.DataTable.add
** (exit) exited in: GenServer.call(#PID<0.157.0>, :add, 5000)
** (EXIT) process attempted to call itself
(elixir) lib/gen_server.ex:598: GenServer.call/3
If I try to modify the call
function to GenServer.call(:my_table, :add)
or GenServer.call(__MODULE__, :add)
, I get this error: ** (EXIT) no process
. Obviously, I'm doing something wrong with the call
.
So I try to directly update the :ets
table:
def add_direct do
:ets.insert(:my_table, {2, "two"})
end
iex(1)> MyApp.DataTable.add_direct
** (ArgumentError) argument error
(stdlib) :ets.insert(:my_table, {2, "two"})
(my_app) lib/my_app/data_table.ex:17:
MyApp.DataTable.add_direct/0
When I run :ets.all()
, I can see :my_table
. So finally I resort to trying to update it in iex
:
iex(2)> :ets.insert(:my_table, {2, "two"})
** (ArgumentError) argument error
(stdlib) :ets.insert(:my_table, {2, "two"})
Just to make sure I am not entirely crazy, I run this sanity check that does work:
iex(2)> :ets.new(:my_table2, [:set, :named_table])
:my_table2
iex(3)> :ets.insert(:my_table2, {2, "two"})
true
I must be going wrong in the server callback and just a fundamental misunderstanding of how :ets
works inside modules.
There are multiple problems with this. I'll try to explain each one:
iex(1)> MyApp.DataTable.add
** (exit) exited in: GenServer.call(#PID<0.157.0>, :add, 5000)
** (EXIT) process attempted to call itself
(elixir) lib/gen_server.ex:598: GenServer.call/3
This is because you're calling a GenServer method on self. You should call that on the PID returned by start_link
.
If I try to modify the call function to GenServer.call(:my_table, :add) or GenServer.call(MODULE, :add), I get this error: ** (EXIT) no process.
The first one fails because :my_table
is not a registered GenServer name. The second one fails because you're not registering the GenServer with a name.
So I try to directly update the :ets table:
This is because ETS tables by default do not allow anyone except the process that created the table to write to the table. You can make the table public by passing :public
as an option to :ets.new
's last argument. This will allow any process to write to that table.
There are many ways to fix this. One is to accept a PID in add
:
def add(pid) do
GenServer.call(pid, :add)
end
And then call it like:
iex(1)> {:ok, pid} = A.start_link
{:ok, #PID<0.86.0>}
iex(2)> A.add(pid)
1
[{2, "two"}]
Another solution is to register the process with a name when you create it:
def start_link do
GenServer.start_link(__MODULE__, :ok, [name: __MODULE__])
end
and then use __MODULE__
in add
:
def add do
GenServer.call(__MODULE__, :add)
end
iex(1)> A.start_link
{:ok, #PID<0.86.0>}
iex(2)> A.add
1
[{2, "two"}]
Registering a process with a name also means you cannot register another process with the same name while the first one is alive, but that's probably fine here since you're using a fixed ETS table name, which are also uniquely named.