Linked process (GenServer) not ending when Task ends

When you spawn and link a process inside a Task, it doesn’t seem to die when the Task ends.

defmodule Foo do
  use GenServer
  def start_link(), do: GenServer.start_link(__MODULE__, nil)
  def init(nil), do: {:ok, nil}
end

iex> {:ok, pid} = Task.async(fn -> Foo.start_link() end) |> Task.await()
iex> Process.alive?(pid)
true

I thought the GenServer process would be linked to the Task and thus end when the Task ends.

Now that I’m saying this out loud, I’m thinking it’s because the Task ends normally, which then doesn’t bring down linked processes?

Is there a way to do this though?

Use case is people are spawning network connection type GenServers inside web requests and background jobs and not shutting them down, so they leak. It would be nice if the connections automatically stopped when the process they are linked to stopped.

Thanks for the help.

The good news is, it’s totally doable.

The bad news is, You should refactor and use appropriate tools, like dynamic supervisor, registry etc.

The BEAM has tools to exchange messages when processes dies, link and monitor.

I would not use Task for this use case.

Doing a little bit more research, the problem doesn’t seem to be related to tasks at all.

iex> pid = spawn_link fn -> IO.inspect(Redix.start_link()) end
#PID<0.2062.0>
{:ok, #PID<0.2063.0>}

iex> Process.alive?(pid)
false

iex> Process.alive?(pid("0.2063.0"))
true

So the only thing I can think of is using some kind of connection manager which monitors the calling process and then automatically cleans up any connections if the calling process ends.

# in application.ex
children = [RedixConnectionManager]

# in web controller/action, background job, etc.
{:ok, pid} = RedixConnectionManager.new_connection()
results = Redix.command!(pid, ["keys", "*"])

Is that too heavy handed or seems about right?

P.S. The Redis connection example is contrived; we use NimblePool around Redix connections so it’s a total non-issue.

1 Like

Yes, like this… with a Process.monitor()

The GenServer does not die because the task ends with a normal exit (no error). If you call exit(:my_error) from the task, or raise something, the GenServer will be killed too.

3 Likes