What is the recomended way to terminate a process if it's unused?

Viewed 189

Let's say I have a process that is used sporadically. Should I just let it be, or save its state to the database and shut it down after some time? Is there an OTP way to do it?

I came up with this idea of a timer process that waits specific amount of time and then notifies my worker process that it has to be shut down. In that way for every worker process there would be one timer process and every time worker process does something it would notify its timer process to reset the timer.

2 Answers

I work on an application that tracks users and we do exactly what you are specifying. It will aggregate certain values for the user and it will shut down after 60 seconds. There is a callback for GenServers handle_continue/2 that you can use to set and reset timers for the process. Make sure you start the processes under a DynamicSupervisor and give them a restart strategy of :temporary

Example:

def MyProc do
  use GenServer, restart: :temporary
  @timeout :timer.seconds(60)

  def start_link(opts) do
    GenServer.start_link(__MODULE__, [], opts)
  end

  def init([]) do
    # This will set the initial state and continue to set_timer
    {:ok, %{timer: nil}, {:continue, :set_timer}}
  end

  def poke(pid) do
    GenServer.call(pid, :poke)
  end

  def do_work(pid) do
    GenServer.cast(pid, :do_work)
  end

  def handle_call(:poke, _from, :state) do
    # We have received a call. Do the work and continue to set_timer
    IO.puts("ouch")
    {:reply, :ok, state, {:continue, :set_timer}}
  end

  def handle_cast(:do_work, state) do
    IO.puts("Working...")
    # You can continue from handle_cast/2 & handle_info/2 and even handle_continue/2
    {:noreply, state, {:continue, :set_timer}}
  end

  def handle_info(:graceful_stop, state) do
    IO.inspect("PID #{inspect(self()} received inactivity shutdown. Bye!")
    {:shutdown, :normal, state}
  end

  def handle_continue(:set_timer, state) do
    # Forget the old timer if it exists
    case state do
      %{timer: timer} when is_reference(timer) -> :timer.cancel(timer)
      _ -> nil
    end
    
    # Send this process the stop message after the timeout expires
    timer_ref = Process.send_after(self(), @timeout, :graceful_stop)
    {:noreply, %{state | timer: timer_ref}}
  end
end

The handle_continue callback is useful for many reasons. First, it will not block the calling process on your init/1 callback. Any heavy initialization of the GenServer should be done with handle_continue. Second, it allows processing to continue work after the process has replied to the caller. For example, we would not want to make the user wait on a response from this process because we are resetting the timer. Third, it allows for code to be shared easily. All you have to do is put {:continue, :set_timer} after each action that indicates the process should remain alive and you can be confident that the timeout will be extended.

Thanks to @Mike Quinlan I've found an even better way to do this using GenServer timeouts. I just included the timeout value in every return value from handle_* callbacks. Example:

defmodule Client do
  use GenServer

  @timeout 5000

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

  def hello(pid) do
    GenServer.call(pid, :hello)
  end

  def init(state) do
    {:ok, state}
  end

  def handle_call(:hello, _from, state) do
    {:reply, :hello, state, @timeout}
  end

  def handle_info(:timeout, state) do
    {:stop, :normal, state}
  end

end

The timer resets every time I call for hello/1. If I don't call hello/1 for 5 seconds after the first call, the process shuts down.

Related