Search code examples
erlangerlang-otperlang-supervisorelixir

DynamicSupervisor - problem with communication with the workers


I have a problem with communication with the workers created in my dynamicSupervisor. after start all my workers i try make a call to one by pid and is generated one error (always). At the beginning of the child I keep the pid with ets. Then when I want to make a call to this child I get the pid from the ets by an identifier. So far so good. The problem is when I do the following:

GenServer.call(
  pid,
  {:action_project, %{project_id: project_id, pid: :erlang.pid_to_list(pid)}}
)

returns following error:

[error] GenServer #PID<0.606.0> terminating
** (RuntimeError) attempted to call GenServer #PID<0.606.0> but no handle_call/3 clause was provided
    (backercamp) lib/gen_server.ex:693: MyApplication.ProjectWorker.handle_call/3
    (stdlib) gen_server.erl:661: :gen_server.try_handle_call/4
    (stdlib) gen_server.erl:690: :gen_server.handle_msg/6
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.680.0>): {:action_project, %{pid: '<0.606.0>', project_id: "23"}}
State: %{pid: '<0.606.0>', project_id: "23"}
Client #PID<0.680.0> is alive

    (stdlib) gen.erl:169: :gen.do_call/4
    (elixir) lib/gen_server.ex:921: GenServer.call/3

What's the problem with this call?

DynamicSupervisor code:

defmodule MyApplication.Supervisor do
  use DynamicSupervisor

  alias MyApplication.Management

  def start_link(arg) do
    DynamicSupervisor.start_link(__MODULE__, arg, name: __MODULE__)
  end

  def init(arg) do
    DynamicSupervisor.init(arg)
  end

  def start_project_worker(project_id) do
    spec = {MyApplication.ProjectWorker, %{project_id: project_id}}
    DynamicSupervisor.start_child(__MODULE__, spec)
  end

  def start_all_project_workers() do
    Enum.each(Management.list_all_projects(), fn %{id: project_id} ->
      IO.puts("Started for project Id [#{project_id}]")
      # 1s between workers
      :timer.sleep(1000)
      start_project_worker("#{project_id}")
    end)
  end

  def action_project(pid, project_id) do
    GenServer.call(
      pid,
      {:action_project, %{project_id: project_id, pid: :erlang.pid_to_list(pid)}}
    )
  end
end

Worker code:

defmodule MyApplication.ProjectWorker do

  use GenServer, restart: :transient

  alias MyApplication.Settings
  alias MyApplication.Management

  def start_link(state) do
    GenServer.start_link(__MODULE__, state)
  end

  def init(state) do
    schedule_action_project(Settings.get_frequency_ms())
    state = Map.put(state, :pid, :erlang.pid_to_list(self()))
    persist_state(state)
    {:ok, state}
  end

  def handle_info(:schedule_action_project, %{project_id: _project_id, pid: _pid} = state) do
    action_by_state(state)
    {:noreply, state}
  end

  def handle_call({:action_project}, %{project_id: _project_id, pid: _pid} = state) do
    case action_by_state(state) do
      true ->
        terminate(state)

      false ->
        {:reply, state, state}
    end
  end

  defp persist_state(state) do
    IO.puts(" :: Add project_id [#{state.project_id}] and pid #{state.pid}")
    :ets.insert_new(:project_backup, {state.project_id, state.pid})
  end

  defp delete_persist_state(project_id) do
    IO.puts(" :: Delete project_id [#{project_id}]")
    :ets.delete(:project_backup, project_id)
  end

  defp schedule_action_project(time) do
    IO.puts(" :: Schedule_action_project [#{time}]")
    Process.send_after(self(), :schedule_action_project, time)
  end

  defp terminate(%{project_id: project_id, pid: _pid} = state) do
    IO.puts(
      " :: Stop processed, everything is done! project_id [#{state.project_id}] and pid #{
        state.pid
      }"
    )

    delete_persist_state(project_id)
    {:stop, :normal, state}
  end

  defp action_by_state(%{project_id: _project_id, pid: _pid} = state) do
    action_by_project(Management.get_project!(state.project_id))
  end

  defp action_by_project(%Project{} = project) do
    #do something in project
  end
end

Solution

  • The issue is within a handler. You messed all the arguments up. state is an internal state of GenServer, you do not pass it through, you receive it in the callback. The following signature will match if your state is a map having keys project_id and pid and you call it like GenServer.call(pid, {:action_project}).

    def handle_call({:action_project}, %{project_id: _project_id, pid: _pid} = state)
    

    You should change this handler to match your caller:

    def handle_call(
      {:action_project, %{project_id: _project_id, pid: _pid}},
      _from,
      state
    )
    

    Also note that handle_call callback receives three parameters (unlike handle_cast, the second one being the source of the message.


    Sidenote: handle_info callback has the wrong signature as well.