Using `Task.start` on Phoenix seeds?

Hi all!

I’m starting my first project with Elixir and Phoenix. I have a function in a context that creates a record and fires some asynchronous code using Task.start. This task fetches data from a local URL and updates the newly created record.

I want to reuse this code for my seed data, but the process seems to finish before the Task.start has finished running, so the seed records are not being updated with the complete data.

The code works as expected from iex -S mix.

Is there any way to force the process to wait until the Tasks are finished, without using Task.async? The only thing that comes to mind is a loop that counts the records in the DB that have not yet been updated, and sleep for a second if there’s any, but I’d like to try to find a better solution.

Thanks!

:wave:

You can enter a receive block, which is what Task.await does. But for that you’d need to send your process a message on task completion to leave that block. The easiest way to do that would probably be Task.async, so why don’t you want to use it?

2 Likes

Refactoring the code so that it can be used with either Task.start/3 or Task.async/3 and Task.await/3 should be the preferred approach - that way async/await could be used in the seeding script.

A more “hackey” solution: start returns {:ok, pid}; monitor the pid with Process.monitor/1 and at the end of the script put a receive/1 for the {:DOWN, ref, :process, object, reason} message.

2 Likes

For clarity, here’s my code.

My seeds.exs file:

alias MyApp.Multimedia
urls = [...] # an array of strings representing URLs

# I try to fetch the URL from my DB, or I create it
# To avoid double seeding the dev DB
Enum.map(urls, fn url -> Multimedia.get_url!(url) || Multimedia.create_url(url) end)
defmodule MyApp.Multimedia do
  def create_url(url) do
    %Url{}
    |> Video.creation_changeset(%{url: url})
    |> create_url
  end

  def create_url(%Ecto.Changeset{} = changeset) do
    case Repo.insert(changeset) do
      {:ok, %Url{} = url} ->
        get_basic_url_info(url)
        {:ok, url}

      other_value ->
        other_value
    end
  end

  def get_basic_url_info(%Url{} = url) do
    Task.start(fn -> Devflix.Brain.create(url.url) end)
  end
end

The problem right now is that when the Enum.map section of the seeds file finishes, all other Tasks are deleted.

I read that the Task.async needs to be used together with Task.await, and I don’t need to check the result during the normal life cycle of the app, only in the seeding part. That’s why I didn’t want to use it, but maybe I understood it wrong?

I finally moved to a mini-loop that checks if the seed data is complete to let the tasks run correctly:

defmodule Loops do
  def while(0), do: :ok
  def while(_n) do
    Process.sleep(1000)
    while(Repo.aggregate(query(), :count, :id))
  end

  def query do
    from(v in Url, where: is_nil(v.data)) # v.data is set via a `Task`
  end
end

Loops.while(Repo.aggregate(Loops.query, :count, :id))

I don’t find it a bad solution, although I guess mine is not the most Elixir-esque one…

Thanks to both, @idi527 @peerreynders

What about Task.yield?

You will immediately avoid the very ugly pattern of waiting fixed amounts and then re-checking.