Hacker News new | ask | show | jobs
by jonnycat 753 days ago
This post got me curious about similar scenarios in Elixir, and despite working with Elixir every day, I'm a bit surprised by one of the results I found:

  # Recursive function that never terminates:
  iex> f = fn i, f -> if rem(i, 100000) == 0 do IO.inspect(i) end; f.(i+1, f) end
  # Start the function in a task with a 1 ms timeout
  iex> Task.async(fn -> f.(0, f) end) |> Task.await(1)
My expectation here is that the task would output 0, then get killed when it hits the timeout. And I do get a timeout "exit" message logged with the child pid. But ALSO, the numbers keep printing as though the child task is still running! It appears to be specific to the configuration of the iex process but I'm not sure what it is - any Elixir/Erlang folks who can explain exactly what is happening here?
3 comments

The source code for Task is very readable but also kind of subtle, and makes for a good study. I would say definitely give it a shot to trace the flow from Task.async[0] to Task.await[1] to Task.Supervised.start_link[2] to Task.Supervised.reply[3]. There is some subtle interplay with regard to waiting for messages/timeouts and process links.

[0] - https://github.com/elixir-lang/elixir/blob/v1.16.3/lib/elixi... [1] - https://github.com/elixir-lang/elixir/blob/v1.16.3/lib/elixi... [2] - https://github.com/elixir-lang/elixir/blob/v1.16.3/lib/elixi... [3] - https://github.com/elixir-lang/elixir/blob/v1.16.3/lib/elixi...

Task.await tries to exit the calling process when the timeout hits, but IEx traps the exit in that process, so it doesn't terminate and thus the linked task process doesn't either, I think? If I do all of this wrapped in another task, rather than directly in IEx, then I observe the innermost process get terminated by the process link after the intervening one doesn't trap the exit.

Relevant from https://hexdocs.pm/elixir/1.4.5/Task.html, which you've probably already seen:

> If the timeout is exceeded, await will exit; however, the task will continue to run. When the calling process exits, its exit signal will terminate the task if it is not trapping exits.

Bletch, I had the wrong version of the documentation bookmarked—here's the revised relevant sentences from https://hexdocs.pm/elixir/1.16.2/Task.html#await/2 (my system has 1.16.2):

> If the timeout is exceeded, then the caller process will exit. If the task process is linked to the caller process which is the case when a task is started with async, then the task process will also exit.

Reasoning is the same though; self() preceding/following it in the IEx session still shows the same evaluator process alive.

Good call - I think you're right about IEx trapping the exit. The confusing part is that it still logs out this message:

  ** (exit) exited in: Task.await(%Task{mfa: {:erlang, :apply, 2}, owner: #PID<0.954.0>, pid: #PID<0.964.0>, ref: #Reference<0.1455049351.208994307.4788>}, 1)
    ** (EXIT) time out
    (elixir 1.14.3) lib/task.ex:830: Task.await/2
Yeah, I agree that the way that comes out is really awkward. The first line there is reporting the context from in the Task module (because task.ex includes it explicitly in the exit call), and then the second is reporting a strerror-like translation of the :timeout reason, but the lines aren't clearly linked together that way and look more like chained events. All of the %Task{} stuff of course is just the inspection of the argument, but if your eye jumps to the “pid” part it can look like it's reporting that that's the exiting process even though it's not. And then the part where the first “exited” is written in past tense as though the exit happened, when in fact it's describing what the trap just caused to not actually fully happen, is probably the most confusing of all.
I never learned Elixir, but my first guess is IO.inspect is sending a message that is printed by a different process. Then the prints after exit are just the IO process working through its mailbox.

Alternatively, the await might killing the waiting process, not the process being waited on.

Good thoughts, but the printing continues indefinitely, and the documentation for Task.await explicitly says the child process will be killed: "If the timeout is exceeded, then the caller process will exit. If the task process is linked to the caller process which is the case when a task is started with async, then the task process will also exit". Processes can be configured with the behavior you describe, but it's not the case with Task.await.