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.
You can enter a receive block, which is what Task.awaitdoes. 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?
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.
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…