Skip to content

Support :on_timeout in Task.async_stream and friends#6009

Merged
whatyouhide merged 10 commits into
elixir-lang:masterfrom
whatyouhide:on-timeout
Apr 28, 2017
Merged

Support :on_timeout in Task.async_stream and friends#6009
whatyouhide merged 10 commits into
elixir-lang:masterfrom
whatyouhide:on-timeout

Conversation

@whatyouhide
Copy link
Copy Markdown
Member

:on_timeout can be :kill_task | :exit, where :exit is the current behaviour.

Comment thread lib/elixir/lib/task.ex Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe ":exit (default) - ..."?

This way we don't need the trailing "Defaults to :exit.".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, will update.

Comment thread lib/elixir/test/elixir/task_test.exs Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we log some messages when we kill?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mh, I don't think so. Should we?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I was just asking/checking. :)

josevalim
josevalim previously approved these changes Apr 18, 2017
Copy link
Copy Markdown
Member

@fishcakez fishcakez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the 2 possible ways to leak timers I think we need to send the timer to the Monitor, instead of the Caller.

I believe that sending the timer/cancelling in the Monitor would also allow us to run slightly faster in a multi core scenario because we can run slightly more efficiently:

  • cancel timer using async: true, info: false so we don't block to cancel (this might be a call to timer wheel on another scheduler) as we wouldn't care for the result and don't need to flush in Monitor
  • send pid to Caller then start timer (don't block to add to timer wheel)

Comment thread lib/elixir/lib/task/supervised.ex Outdated
Copy link
Copy Markdown
Member

@fishcakez fishcakez Apr 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When starting a timer for a different process it is not possible to guarantee that the other process receives the timer reference because it is always possible for the process to get an exit signal in between starting the timer and sending the timer reference. Therefore to prevent timer leaks we would require the different process to always go down, i.e. using links, monitors or supervisors. Unfortunately can't guarantee this here.

It could happen in the following situation:

  • on_timeout: :exit and catches exit when stream enumerated
  • Monitor process gets exit signal that makes it exit between Process.send_after and send
  • Caller is trapping exits when it receives the exit signal from monitor process (need not be when spawning monitor process but still possible if :kill)

Comment thread lib/elixir/lib/task/supervised.ex Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the caller kills the timed out task and is not trapping exits it will get :killed exit signal from the propagating links. Would it be more useful to have semantics that match Task.yield and Task.shutdown, where we would have nil on timeout? (Note that Task.shutdown does a safe unlink+shutdown/kill).

Copy link
Copy Markdown
Member Author

@whatyouhide whatyouhide Apr 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nil + safe kill sounds okay to me, @josevalim thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and yes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also test the safe kill, possibly by not trapping exits during the tests.

Comment thread lib/elixir/lib/task/supervised.ex Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There can be pending timers in our waiting map, we would need to cancel all these timers on stream_close, as the Caller might catch the exit/1.

Comment thread lib/elixir/lib/task/supervised.ex Outdated
case waiting do
%{^position => {_, {:ok, _} = ok}} -> Map.put(waiting, position, {nil, ok})
%{^position => {_, :running}} -> Map.put(waiting, position, {nil, {:exit, reason}})
%{^position => {_, :timed_out}} -> Map.put(waiting, position, {nil, {:exit, :killed}})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can not guarantee the reason is :killed here, it can be any reason. For example task can exit while timer message is in the message queue of the Monitor. Since it would be :killed if it was killed we could just use the reason in the :exit. Also the reason might be :killed because the task was killed for another cause.

Therefore I think we should use another value when :timed_out and reason is :killed, otherwise do {:exit, reason}.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fishcakez okay, got it. So one option would be to Process.exit(task, :kill) but emit {:exit, :killed_for_timeout}, and another option would be to Process.exit(task, :kill) and emit {:exit, reason_task_died}, did I get it correctly? If so, which one do you think is better?

case running_tasks do
%{^ref => {position, _type, pid, _timer_ref}} ->
send(parent_pid, {:killed_for_timeout, {monitor_ref, position}})
Process.exit(pid, :kill)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we aren't trapping exits and on_timeout: :exit this creates 2 possibilities.

Either the task is killed and the exit signals propagates causing Caller to get :killed exit signal. Or the Caller handles the :killed_for_timeout message and tells the Monitor to stop all tasks. The Monitor receives the stop and then traps exits before task exits. Then the Caller will call exit({:timeout, ..}). This differing semantics is complex. I think we would need to unlink from the Monitor, perhaps similar to Task.shutdown.

send(parent_pid, {:killed_for_timeout, {monitor_ref, position}})
caller = self()
ref = make_ref()
enforcer = spawn(fn ->
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If trapping exits we could skip spawning, unsure if worth complexity of that optimisation though.

Comment thread lib/elixir/lib/task/supervised.ex Outdated
%{^position => {_, {:ok, _} = ok}} -> Map.put(waiting, position, {nil, ok})
%{^position => {_, :running}} -> Map.put(waiting, position, {nil, {:exit, reason}})
%{^position => {_, :timed_out}} -> Map.put(waiting, position, {nil, {:exit, :killed}})
%{^position => {_, :timed_out}} -> Map.put(waiting, position, {nil, {:exit, :timed_out}})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GenServer.start_link does {:error, :timeout} : https://github.com/erlang/otp/blob/cd412d911efbda23e7dd3aef5cf910defc886211/lib/stdlib/src/proc_lib.erl#L350

Should we use :timeout as well?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, works for me. @josevalim okay for you?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

@whatyouhide whatyouhide dismissed stale reviews from josevalim and fishcakez April 26, 2017 21:12

Need new final review.

Comment thread lib/elixir/lib/task/supervised.ex Outdated
case waiting do
%{^position => {_, {:ok, _} = ok}} -> Map.put(waiting, position, {nil, ok})
%{^position => {_, :running}} -> Map.put(waiting, position, {nil, {:exit, reason}})
%{^position => {_, :timed_out}} -> Map.put(waiting, position, {nil, {:exit, :timeout}})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should use :timeout here too for consistency?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that, I wasn't sure. I don't have any argument for :timed_out here though, so I updated to use :timeout. Should I change the message we send with Process.send_after/3 as well?

Copy link
Copy Markdown
Member Author

@whatyouhide whatyouhide Apr 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably yes :), I updated that as well. Let me know how it looks.

Copy link
Copy Markdown
Member

@josevalim josevalim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good to me. If @fishcakez agree, we shall merge it.

@whatyouhide whatyouhide merged commit dfd0ab5 into elixir-lang:master Apr 28, 2017
@whatyouhide whatyouhide deleted the on-timeout branch April 28, 2017 19:15
@whatyouhide
Copy link
Copy Markdown
Member Author

🎉!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants