i’m trying to run my tests but i got a : the table identifier does not refer to an existing ETS table error everytime.
what happens is we have a file inside phoenix called context.ex and there I have this piece of code:
defp store_user_in_ets(user_type) do
:ets.new(:user_registry, [:set, :protected, :named_table])
:ets.insert(:user_registry, {"user", user_type})
end
then everytime a users login this file gets his data a passes to this function so we get if he is a regular user or a admin
then in myapp/audit_logs/audit_log.ex I have this code:
defp get_user_by_ets do
user =
case :ets.lookup_element(:user_registry, "user", 2) do
%{current_admin_user: %{admin_user: user}} -> user
[] -> {:erro, nil}
end
{:ok, user}
end
and this I think where the problem happens in my tests:
test/data_layer/site_home_banners_test.exs:28
** (ArgumentError) errors were found at the given arguments:
* 1st argument: the table identifier does not refer to an existing ETS table
code: site_home_banner = site_home_banner_fixture()
stacktrace:
(stdlib 3.17.2) :ets.lookup_element(:user_registry, "user", 2)
(data_layer 0.0.2) lib/data_layer/audit_logs/audit_log.ex:41: DataLayer.AuditLogs.AuditLog.get_user_by_ets/0
(data_layer 0.0.2) lib/data_layer/audit_logs/audit_log.ex:10: DataLayer.AuditLogs.AuditLog.handle/3
(data_layer 0.0.2) lib/data_layer/site_home_banners.ex:90: DataLayer.SiteHomeBanners.create_site_home_banner/2
test/data_layer/site_home_banners_test.exs:23: DataLayer.SiteHomeBannersTest.site_home_banner_fixture/1
test/data_layer/site_home_banners_test.exs:29: (test)
So you can put it in a Genserver and start Genserver in top of your test or in setup
. or you can do somthing like this:
case ETS.Set.wrap_existing(@ets_table) do
{:ok, set} -> set
_ ->
start_link([]) # or start your ets
table()
end
1 Like
so what this ETS.Set.wrap_existing does?
It is a warper of table info :ets.info(table)
, I am using GitHub - TheFirstAvenger/ets: :ets, the Elixir way library
ETS.Set — ets v0.9.0
If you are using :ets
, you can call your table name or reference of the table based on your function. If table does not exist, so it tries to create it, and you can use setup
and setup_all
based on your requirement in unit test.
Check this out…
Open a new IEx repl and do this…
iex> :ets.new(:user_registry, [:set, :protected, :named_table])
:user_registry
iex> ets.info(:user_registry)
[
id: #Reference<0.3400188531.1392902147.260966>,
decentralized_counters: false,
read_concurrency: false,
write_concurrency: false,
compressed: false,
memory: 313,
owner: #PID<0.341.0>,
heir: :none,
name: :user_registry,
size: 0,
node: :nonode@nohost,
named_table: true,
type: :set,
keypos: 1,
protection: :protected
]
Now open a new IEx repl and do this:
iex> spawn(fn -> :ets.new(:user_registry, [:set, :protected, :named_table]) end)
#PID<0.438.0>
iex> ets.info(:user_registry)
:undefined
Your store_user_in_ets
function is creating the ETS table if it doesn’t exist. The problem is that it’s only alive as long as the process that spawned it is alive.
You should probably make a global :user_registry
that is created when your app starts up.
Tests start a bunch of processes. Your :user_registry
was created in an ephemeral process, thus when that ephemeral process ends, other processes don’t have access to :user_registry
anymore.
2 Likes
Adding a little if OP is unfamiliar with where to start making a long running table.
You can start the table in the init/1
callback of a GenServer that will be the table owner. I like to wrap table access in an API that is defined in the same module as the GenServer.
defmodule UserRegistry do
use GenServer
## API
def store_user(user_type) do
:ets.insert(__MODULE__, {"user", user_type})
end
# ... more API functions
# the GenServer doesn't need a name because it will never be called
def start_link(arg), do: GenServer.start_link(__MODULE__, arg)
@impl true
def init(_arg) do
# using __MODULE__ as the name for convenience, table is only accessed by API functions defined here
:ets.new(__MODULE__, [:set, :public, :named_table])
{:ok, []}
end
@impl true
# catch all unexpected messages and log them
def handle_info(msg, state) do
Logger.warn("UserRegistry received unexpected message: #{inspect(msg)}")
{:noreply, state}
end
end
If the process dies and restarts then so does the table, so the GenServer implementation is as thin as possible and all unexpected “regular” messages are handled by a catch-all callback. Access is public
so you can still insert and lookup from any process, and in this case no access is performed by the owning process itself. Then you must add the UserRegistry
GenServer to your supervision tree so the table always starts on application startup (including in tests) and (only) restarts in case of exception.
# lib/my_app/application.ex
def start(_args, _type) do
children = [
UserRegistry,
...
]
opts = [...]
Supervisor.start_link(children, opts)
end
Resources:
GenServer
ETS
1 Like