How to properly create handle_info with a list of Task?

In handle_info function I have this:

def handle_info(:tick, items) do
  items2 = do_job(items)
   tick()
  {:noreply, items2}
end

In “do_job” I need to a) iterate throug items, b) make an http request which can take long and c) depending on a response, update a database and finish with the current item by removing it from “items” or merely update the database:

def do_job(items) do
  Enum.each(items, fn(a) -> # or Enum.map
    Task.start fn ->
      
      case external_request(a) do
        {:terminate, data} ->
          Repo.update(....)
          remove(a) # send a message to this GenServer to remove itself

        {:continue, data} ->
          Repo.update(....)
      end

    end
  end)
end
  1. Even if I had Enum.map, how would I return a value fro do_job since it creates Task for each one and executes asyncronously?

  2. Do I have to return it at all? I think I do because handle_info should return a tuple with a list of items in case of success

  3. Should I use Task.start or maybe better Task.async and wait? If so, how exactly for my case? Yet the #2 will remain.

1 Like

Can you talk about your use case more? Your question is largely about how to implement a particular approach, but that approach may not be the best for what you’re doing. If you tell us what you’re trying to do, it will likely help us to give you a better answer.

As your do_job/1 is right now it transforms (maps) a set of items to a set of tasks. If you want values from the tasks then have the task callback either send a message or return a value and await on the tasks. :slight_smile:

You always have a return, right now you are returning a list of tasks equal to the original items.

All depends on what you want to return and ‘how’?

1 Like

Also note:
Task.await/2 Compatibility with OTP behaviours

It is not recommended to await a long-running task inside an OTP behaviour such as GenServer. Instead, you should match on the message coming from a task inside your GenServer.handle_info/2 callback.

This - somewhat dated - article could be a starting point: The Erlangelist: Beyond Task.Async 2015-07-31

The article demonstrates the approach with a raw “master” process - so the code still needs to be “ported” for use in a GenServer.

Edit: the article uses Task.find/2 which has been deprecated in Elixir 1.3.0

[Task] Task.find/2 is deprecated in favor of explicit message matching.

2 Likes

Having a list of items, iterate over them send an http request. After a response from a remote server has been received, update a value in a database. Also depending on a response , delete delete that value from a list or do the next iteration.

all items are unique. each new item is too.

I suspect Ben’s request is motivated by the X-Y Problem - prompted by your handle_info/2 function which, without context can seem peculiar for the following reasons:

  • The primary design objective of a GenServer is usually handled via it’s handle_call/3 and handle_cast/3 callbacks. handle_info/2 is primarily used to process raw “fringe” messages that aren’t generated via call/3 and cast/2.
  • There seems to be an external message source emitting :tick messages for this GenServer. Why isn’t cast/2/handle_cast/2 being used instead?
  • The GenServer state is simply a list of items that seem to be “polled” for some reason. What’s stopping those :tick messages from coming in faster than those tasks completing? You could be starting tasks on items that previous tasks end up deleting. Wouldn’t it make more sense for the GenServer to manage it’s own “tick” based on when all the previous task complete?

Basically that handle_info function looks “fishy” without a larger context that may explain why it is the way it is.

the purpose of :tick message is to have a task occur periodically.

each new item is unique.

Does the GenServer generate the :tick message in this fashion - or does it rely on an external source?

Presumably the GenServer should terminate once all the “items” have terminated.

What impact should it have when one of the tasks fails?

i.e. start_link/3 vs. start/3.

Should the failed “item” be removed from the GenServer's state?

GenServer generates it

Presumably the GenServer should terminate once all the “items” have terminated.

no

What impact should it have when one of the tasks fails?
Should the failed “item” be removed from the GenServer’s state?

doesn’t matter

Just a suggestion.

In may make sense to cast/2 a :do_job request from the handle_info/2 callback so that the GenServer's primary responsibility is in a handle_cast/2 callback (rather than in handle_info).

Depending what you are doing it may also make sense to store the “item” together with it’s last task in the GenServer state so that you can skip items with tasks that haven’t completed yet - if that matters in your use case.

  • Task.create is not a function. Consider Task.start instead.
  • Enum.each returns :ok, not a list. Unless you’re updating items, you do not need to return an updated items. You can simply pass items straight through:
    def handle_info(:tick, items) do
        tick()
        do_job(items)
        {:noreply, items}
    end
    

https://hexdocs.pm/elixir/Task.html

why not Task.async and await?

  1. what do you mean? updating where?

  2. when I modify a list or state by adding or removing an element, what makes GenServer know that a list has changed and pickup a new list or state? For example:

    def add_item(x) do
    GenServer.cast(MODULE, {:add, a})
    end

    def handle_cast({:add, a}, state) do
    state ++ a
    end

How does state ++ a make GenServer take a return value and update the state so that the state will contain one element more after that?

Task.async and Task.await are used when you wish to compute a value asynchronously and then retrieve that value. I don’t think you’re trying to do that here, though I may be wrong.

What I’m doing has been explained: for each item in a list a) sending http request b) making an update in a database

Why would I do that syncronously especially on a list of items?

That handle_cast({:add, a}, state) function will fail. You need to return {:noreply, updated_state}. Additionally, state ++ a is not the proper way to add an item to a list. Use [a | state] to prepend (fast) and state ++ [a] to append (slow).

Using Task.start will still perform the task asynchronously, you just won’t be given a reference to retrieve the return value of the task at a later point.

https://hexdocs.pm/elixir/Task.html

1 Like

yes[quote=“net, post:17, topic:4854, full:true”]
That handle_cast({:add, a}, state) function will fail. You need to return {:noreply, updated_state}. Additionally, state ++ a is not the proper way to add an item to a list. Use [a | state] to prepend (fast) and state ++ [a] to append (slow).
[/quote]

prepend, append, slower, faster – that doesn’t matter and my question isn’t about that.