Search code examples
elixirgen-serverets

Elixir: Argument Error while accessing ETS from an Elixir Task


I'm currently trying to build a Redis clone in Elixir. As part of that effort, I'm using a task to handle a request to get/set and I'm using ETS to store the Keys and Values.

** (ArgumentError) argument error
    (stdlib 3.13.2) :ets.insert(:kv, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})

Here are the steps I took with the relevant code sections:

  1. I created a public ETS table
 def init(_) do
    :ets.new(:kv, [:set, :public, :named_table])
  end
  1. As per the example in Elixir School I attempt to insert and read from the ETS table
 :ets.insert(:kv, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
    IO.inspect(:ets.lookup(:kv, "doomspork"))
  1. Unfortunately, this gives me the error described here:
[error] Task #PID<0.159.0> started from #PID<0.156.0> terminating
** (ArgumentError) argument error
    (stdlib 3.13.2) :ets.insert(:kv, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
    (redis 1.0.0) lib/server.ex:61: Server.write_line/2
    (redis 1.0.0) lib/server.ex:41: Server.serve/1
    (elixir 1.11.0) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
    (stdlib 3.13.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
Function: #Function<0.28410304/0 in Server.loop_acceptor/1>
    Args: []

I was hoping to understand why the error is occurring or how to get suggestions on how I might go about debugging this problem. Any help would be deeply appreciated.

I've listed the entire .ex file for reference. General feedback about the code as a whole is welcome too -- I'm relatively new to Elixir and would love to hear your suggestions so that I can improve.


defmodule Server do
  @moduledoc """
  Your implementation of a Redis server
  """
  use Application
  require Logger

  def start(_type, _args) do
    Supervisor.start_link(
      [{Task.Supervisor, name: Server.TaskSupervisor}, {Task, fn -> Server.listen() end}],
      strategy: :one_for_one
    )
  end

  def init(_) do
    :ets.new(:kv, [:set, :public, :named_table])
  end

  @doc """
  Listen for incoming connections
  """
  def listen() do
    {:ok, socket} = :gen_tcp.listen(6379, [:binary, active: false, reuseaddr: true])
    Logger.info("Accepting connections on 6379")
    loop_acceptor(socket)
  end

  defp loop_acceptor(socket) do
    {:ok, client} = :gen_tcp.accept(socket)
    {:ok, pid} = Task.Supervisor.start_child(Server.TaskSupervisor, fn -> serve(client) end)
    :ok = :gen_tcp.controlling_process(client, pid)
    loop_acceptor(socket)
  end

  defp serve(client) do
    client
    |> read_line()
    |> write_line(client)

    serve(client)
  end

  defp read_line(socket) do
    {:ok, data} = :gen_tcp.recv(socket, 0)
    data
  end

  defp echo(socket, command_arr) do
    echo_statement = Enum.at(command_arr, -2)
    IO.inspect(echo_statement)
    :gen_tcp.send(socket, "+#{echo_statement}\r\n")
  end
  defp write_line(line, socket) do
    command_arr = String.split(line, "\\r\\n")
    command = Enum.at(command_arr, 2) |>String.downcase
    IO.inspect(command_arr)
    IO.inspect(command)
    :ets.insert(:kv, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
    IO.inspect(:ets.lookup(:kv, "doomspork"))
    case command do
      "ping" -> :gen_tcp.send(socket, "+PONG\r\n")
      "set"-> :ets.insert(:kv, {"samplekey", "sampleresp"})
      "get"-> :ets.lookup(:kv, "samplekey")
      "echo"-> echo(socket,command_arr)
      _ -> :gen_tcp.send(socket, "Nah")
    end
  end
end

Solution

  • You are trying to access ETS before it was created. init/1 callback is called within the process after it’s started, but the first access attempt happens from the task run by start which might happen earlier. In your case, this is an Application which does not have init/1 callback at all.

    Simply move :ets.new(:kv, [:set, :public, :named_table]) to start/2 before anything else.