Proposal: Add function to return the first async task to complete

113 views
Skip to first unread message

thia.md...@gmail.com

unread,
Mar 31, 2021, 4:54:38 PM3/31/21
to elixir-lang-core
Proposal

Add a function to the Task module that takes a list of tasks, and returns as soon as one of the tasks finishes, shuting down the other tasks.
The behaviour would pretty similar to what Javascript have with Promise.any https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any

Motivation

One scenario that it could be useful is when we are integrating with multiple APIs (providers) of the same data, and we want only the fastest result without needing to wait for the other requests to complete.

Today I think this could be implemented with something similar to the following code:

tasks = [
  Task.async(&heavy_fun_1/0),
  Task.async(&heavy_fun_2/0),
  Task.async(&heavy_fun_3/0)
]

receive do
  {ref, result} ->
    tasks
    |> Enum.reject(fn task -> task.ref == ref end)
    |> Enum.each(&Task.shutdown/1)

    result
after
  5000 ->
    {:error, :timeout}
end

However that seems to be a common enough pattern to add to the standard library.

Questions

- Am I missing something here and this could already be easily accomplished with the existing API?
- What should be the behaviour when the first task to complete exits?

Wojtek Mach

unread,
Mar 31, 2021, 5:06:47 PM3/31/21
to elixir-l...@googlegroups.com
Check out Task.yield_many/2 (https://hexdocs.pm/elixir/Task.html#yield_many/2) :-)

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/59a5b5af-528b-4f1f-8e17-6dad9edfe9ccn%40googlegroups.com.

thia.md...@gmail.com

unread,
Mar 31, 2021, 5:52:09 PM3/31/21
to elixir-lang-core
I think the proposal would work differently from yield_many. yield_many "receives a list of tasks and waits for their replies in the given time interval".
The proposal of the new function is to return as soon as the first task finishes.

For example, if we start tasks to make call to 3 different remote APIs, with different response times.

API A - 50ms
API B - 500ms
API C - 1500ms

tasks = [
  Task.async(&api_a/0),
  Task.async(&api_b/0),
  Task.async(&api_c/0)
]

# returns result of API A and API B waiting for 1000ms
Task.yield_many(tasks, 1000)

With run using `yield_many` we would wait for the 1000ms and get the responses of API A and API B.

The proposal of the new function is to return as soon as we get a response.
Using the same example of the 3 calls, we would wait only for 50ms (as soon as the first task finishes) and return the result of the first task finishing, without waiting for the other call.

tasks = [
  Task.async(&api_a/0),
  Task.async(&api_b/0),
  Task.async(&api_c/0)
]

# returns only result of API A waiting for 50ms
Task.proposed_function(tasks)

José Valim

unread,
Mar 31, 2021, 6:11:38 PM3/31/21
to elixir-l...@googlegroups.com
It was not possible to implement yield_first in Elixir but now that we have map_get in guards, we can easily do so by putting all refs in a map and only getting messages from the inbox where the ref is in the map. The number of tasks to wait and the maximum timeout should be configurable too. For example:

    yield_first(task, 3, 1000)

The above will yield the first 3 tasks within 1000ms. It should have the same result type as yield_many. In this sense, yield_many becomes a special case of yield_first where you want to yield on all given tasks.

Another option is to not introduce a new function but instead introduce a new argument to yield_many with the limit to yield:

    yield_many(task, 1000, 3)



Felipe Stival

unread,
Mar 31, 2021, 6:26:54 PM3/31/21
to elixir-l...@googlegroups.com
+1 for yield_first(tasks, n, timeout)


Thiago Santos

unread,
Mar 31, 2021, 6:35:47 PM3/31/21
to elixir-l...@googlegroups.com
+1 for yield_first(tasks, n, timeout)

It seems to better convey the meaning "yield the first n tasks".

You received this message because you are subscribed to a topic in the Google Groups "elixir-lang-core" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elixir-lang-core/ZIFsisK12CM/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CAKC64%2BwFh20dRTVnJi5QC8Ekk5CNcSx8k8jW_xBP65rOYGukYw%40mail.gmail.com.

thia.md...@gmail.com

unread,
Apr 1, 2021, 9:57:23 AM4/1/21
to elixir-lang-core
> In this sense, yield_many becomes a special case of yield_first where you want to yield on all given tasks.

I'm not sure if we would want that, because in yield_many the returned list will be in the same order as the tasks supplied in the tasks input argument.
However yield_first to preserve the semantics of "returning as soon it finishes", maybe we should return the tasks in the order of finish (the first to complete first in the list).
That makes sense?

José Valim

unread,
Apr 1, 2021, 10:00:14 AM4/1/21
to elixir-l...@googlegroups.com
I don't see a reason why we wouldn't return the same order. You can easily get the ones you want by `for {task, {:ok, value}} <- result do`. Plus forcing people to iterate will remind them that they most likely need to shutdown the other tasks.

José Valim

unread,
Apr 2, 2021, 3:51:53 AM4/2/21
to elixir-l...@googlegroups.com
I have been thinking more about this problem and there is still one issue for us to solve: when you say you are waiting for "n" tasks, does it mean you are waiting for "n" successes or "n" results (regardless if they are success or failure)?

So I thought perhaps a better solution is to introduce something called Task.yield_stream/2. "yield_stream" receives an enumerable of tasks and emits them as they complete. To get the first successful task you would do:

    tasks
    |> Task.yield_stream(ordered: false, timeout: 5000)
    |> Stream.filter(&match?({:ok, _}))
    |> Enum.at(0)

And this made me wonder: is there any reason why you can't use Task.async_stream?

Leandro Henrique

unread,
Apr 2, 2021, 7:39:18 AM4/2/21
to elixir-lang-core
Funny that exactly on this day that the proposal was opened I said in two groups the lack that I was doing something similar to "Promise.any" in Elixir.

I needed it and found it complicated to do now, I had a job because I'm starting in Elixir.

I would love to have another solution for what I did, when researching several APIs to return the CEP query to me and get the first valid result.

I'm starting in Elixir and in love, and I made my first package with exactly this need: https://github.com/ciareis/cep-promise-elixir/blob/master/lib/cep_promise.ex#L52

I could probably write everything in a better way, but I still get there.

Please implement something to facilitate this;)

Elixir S2!

thia.md...@gmail.com

unread,
Apr 2, 2021, 3:02:22 PM4/2/21
to elixir-lang-core
> And this made me wonder: is there any reason why you can't use Task.async_stream?

After testing, I'm sure we can accomplish the same thing that was proposed with Task.async_stream. There is really no reason for an extra function.

One thing that would be nice is to have an example for this usage in the documentation. I honestly never thought in using Task.async_stream in this way. 

WDYT? PRs for adding this example to the documentation would be welcome?

José Valim

unread,
Apr 2, 2021, 3:31:04 PM4/2/21
to elixir-l...@googlegroups.com
PRs to improve the docs is always welcome!

Reply all
Reply to author
Forward
0 new messages