Compile error for ETS example

When trying to work through the The Getting Started examples I am trying to do ETS as cache. However,

def init(table) do
    # 3. We have replaced the names map by the ETS table
    names = :ets.new(table, [:named_table, read_concurrency: true])
    refs  = %{}
    {:ok, {names, refs}}
  end

causes a compilation error
15:36:25.995 [notice] Application elixirtest exited: KV.start(:normal, []) returned an error: shutdown: failed to start child: KV.Registry
** (EXIT) an exception was raised:
** (ArgumentError) errors were found at the given arguments:

  • 1st argument: not an atom

        (stdlib 3.17) :ets.new({:ok, KV.Registry}, [:named_table, {:read_concurrency, true}])
        (elixirtest 0.1.0) lib/kv/registry.ex:41: KV.Registry.init/1
        (stdlib 3.17) gen_server.erl:423: :gen_server.init_it/2
        (stdlib 3.17) gen_server.erl:390: :gen_server.init_it/6
        (stdlib 3.17) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
    

** (Mix) Could not start application elixirtest: KV.start(:normal, []) returned an error: shutdown: failed to start child: KV.Registry
** (EXIT) an exception was raised:
** (ArgumentError) errors were found at the given arguments:

  • 1st argument: not an atom

        (stdlib 3.17) :ets.new({:ok, KV.Registry}, [:named_table, {:read_concurrency, true}])
        (elixirtest 0.1.0) lib/kv/registry.ex:41: KV.Registry.init/1
    

I have tried a number of changes but none helps. What is wrong?

You’re passing in {:ok, KV.Registry} to init/1, which is passing it as the first argument to to :ets.new/2.

:ets.new/2 expects an atom as the first argument, but you’re passing in a tuple.

The part of the code that says the arguments passed to init/1 is not in your post, so I can’t fix it, but I hope the explanation helps.

2 Likes

Though I have tried to find where i set table to {:ok, KV,Registry} I have not found it. Here comes the module

  use GenServer

  ## Client API

  @doc """
  Starts the registry with the given options.

  ':names' is always required.
  """
  def start_link(opts) do
    # 1. Pass the name to GenServer's init
    server = Keyword.fetch(opts, :name)
    GenServer.start_link(__MODULE__, server, opts)
  end

  @doc """
  Looks up the bucket pid for 'name stored in 'server'.
  Returns '{:ok, pid}' if the bucket exists, ':error' otherwise.
  """
  def lookup(server, name) do
    # 2. Lookup is now done directly in ETS, without accessing the server
    case :ets.lookup(server, name) do
    [{^name, pid}] -> {:ok, pid}
    [] -> :error
    end
  end

  @doc """
  Ensures there is a bucket associated with the given 'name' in 'server'.
  """
  def create(server, name) do
    GenServer.cast(server, {:create, name})
  end
  
  ## Defining GenServer callbacks

  @impl true
  def init(table) do
    # 3. We have replaced the names map by the ETS table
    names = :ets.new(table, [:named_table, read_concurrency: true])
    refs  = %{}
    {:ok, {names, refs}}
  end

  # 4. The previous handle_call callback for lookup was removed

  @impl true
  def handle_cast({:create, name}, {names, refs}) do
    # 5. Read and write to the ETS table instead of the map
    case lookup(names, name) do
      {:ok, _pid} -> 
        {:noreply, {names, refs}}
      :error ->
        {:ok, pid} = DynamicSupervisor.start_child(KV.BucketSupervisor, KV.Bucket)
        ref  = Process.monitor(pid)
        refs = Map.put(refs, ref, name)
        :ets.insert(names, {name, pid})
        {:noreply, {names, refs}}
    end
  end

  @impl true
  def handle_info({:DOWN, ref, :process, _pid, _reason}, {names, refs}) do
    # 6. Delete from the ETS table instead of the map
    {name, refs} = Map.pop(refs, ref)
    :ets.delete(names, name)
    {:noreply, {names, refs}}
  end

  @impl true
  def handle_info(_msg, state) do
    {:noreply, state}
  end
  
end```

Please use triple backtick (```) blocks to format code.

2 Likes

OK, like this?

The mistake is on this line:

server = Keyword.fetch(opts, :name)

Keyword.fetch/2 returns {:ok, value} if the key exists in the keyword list, and :error if it doesn’t. So your server variable receives {:ok, KV.Registry}.

I believe the example uses Keyword.fetch!/2 (note the !), which will raise if the key is not given, and just return value if it exists. Your alternatives are:

# Keyword.fetch!/2, used in the guide
server = Keyword.fetch!(opts, :name)

# pattern matching, will have the same effect as Keyword.fetch!/2, but an uglier error
{:ok, server} = Keyword.fetch(opts, :name)

# pattern matching to give a custom error:
case Keyword.fetch(opts, :name) do
  {:ok, server} when is_atom(server) -> # ...
  :error -> raise ArgumentError, "option `:name` is required to be given for KV.Registry and should be an atom"
end

# Default value
server = Keyword.get(opts, :name, :default_name)
server = opts[:name] || :default_name

# Same as above, but with no default (not recommended!)
server = Keyword.get(opts, :name)
server = opts[:name]
1 Like

Thanks! Perfect