Process.exit does not exit with correct reason

Background

I have a GenServer process, that i need to kill if it tries to do an action and fails. I am able to kill the process, but it is not sending the correct message to it’s supervisor.

Code

Let’s assume I have the following code:

@impl GenServer
def handle_info({:critical_action}, state) do
  case Logic.perform_action(state) do
    {:ok, _res}  ->  {:noreply, Logic.update_state(state)}
    :error       ->  Process.exit(self(), :critical_action_failed)
  end
end

So, upon receiving a message, my GenServer process will attempt to execute it. Because this action is complex, it may fail and if it does so I want it to die and to notify its supervisor or whoever created it (the father process is trapping exits).

Problem

According to the documentation:

The following behaviour applies if reason is any term except :normal or :kill :

(…)
2. If pid is trapping exits, the exit signal is transformed into a message {:EXIT, from, reason} and delivered to the message queue of pid .

So, I should expect a message of type {:EXIT, _, :critical_action_failed}, as seen in the following test:

test "kills worker if critical operation fails" do
      Process.flag :trap_exit, true

      args = [1, 2, 3]
      {:ok, _conn} = Worker.start_link(args)

      assert_receive {:EXIT, _, :critical_action_failed}
      Process.flag :trap_exit, false
    end

This code should pass. Yet it fails because of what I am actually getting:

 No message matching {:EXIT, _, :critical_action_failed} after 100ms.
     Process mailbox:
       {:EXIT, #PID<0.301.0>, {:bad_return_value, true}}

Question

What am I missing? Why am I not getting the expected message ?

Your handle_info callback is returning the value from Process.exit/2, that is, true. This is not a valid return value from such a callback, and you can observe that the process crashes with the reason {:bad_return_value, true}.

Process.exit/2 sends a message to the target process for it to exit, but since you are sending it to self, it will not be processed before the current callback has finished running. When it finishes, GenServer notices your wrong return value and kills the process before it can process that exiting request. So your exit does not have time to be applied.

2 Likes

So, how can I fix this?

Running with the following code doen’t work either:

  case update do
      {_new_val, _old_val}  ->  {:noreply, state}

      :error                ->
        Process.exit(self(), :bla)
        {:noreply, state}
    end

I instead get the following error:

 test/unit/worker_test.exs:385
 No message matching {:EXIT, _, :bla} after 500ms.
 The process mailbox is empty.

Not sure why it doesn’t work, hope someone else can help with that.

1 Like

Perhaps handle_info isn’t firing because of the misspelling :ciritical_action instead of :critical_action

Your worker is trapping exits. Is that intended?

(edited as terminate doesn’t actually matter here - rather, your worker would be getting the exit as a message)

HA, oops. Fixed the example. The problem remains :stuck_out_tongue:

Process.exit returns true which isn’t an appropriate return value for a callback function (:bad_return_value).

Use {:stop, reason :: term(), new_state} instead of Process.exit.


If you are surprised that Process.exit even returns - it simply sends a signal

Erlang -- Communication in Erlang

The amount of time that passes between a signal is sent and the arrival of the signal at the destination is unspecified but positive.

4 Likes

The easiest is to use Kernel.exit/0 or actually just exit/0. So

    case update do
      {_new_val, _old_val}  ->  {:noreply, state}
      :error                ->
        exit(:bla)
    end

The reason for this is that Process.exit/2 does not directly terminate the process but sends an exit signal to it and then it depends on the receiving process how this is handled. If the process is trapping exits then it will result in an exit message in the message queue.

So in your case the GenServer will detect the bad return value and terminate with that and not because of the exit signal.

6 Likes