0
votes

I have a system that creates a Question and inserts this question into the database, I do this by clicking a link that I've set up. This is straightforward Phoenix stuff. Create a controller action setup a link for it and then click the button to fire that action.

This works for now. But the next phase is to have the system create a question without any intervention from the UI. So, that leads me to a new place with Elixir/Phoenix. My new problem is: I need to run this function automatically at x time of day.

Question:

What is the most idiomatic way of implementing a background task in Elixir/Phoenix? I know very little about Genserver or Supervisors but I think I'm ready to start learning about these tools. With all that being said, how would you approach a the problem of moving logic to a background job.

CODE:

  def build_question(conn, _params) do
    case Questions.create_total_points_question(conn) do
      {:ok, _question} ->
        conn
        |> put_flash(:info, "Question created successfully.")
        |> redirect(to: page_path(conn, :index))
      {:error, _msg} ->
        conn
        |> redirect(to: page_path(conn, :index))
    end
  end 

This controller action is triggered from a link. This code needs to be called in the background. Thanks for the help in advance.

2
The code that redirects the user to some other page cannot be run asynchronously. - Aleksei Matiushkin
Gotcha. So, that is something that needs to change. But how would I run Questions.create_total_points_question in the background? - Bitwise
Ahh, interesting. I think those may do what I need. - Bitwise

2 Answers

1
votes

You have few options.

One is to simple run Task.async in your action, but that links process which executes your action with the one you spawned, so task crash will affect process which spawned it and awaits it. BTW, in your particular case I don't think you want this in your action since you don't have anything to work with while awaiting for the result, so it is unnecessary.

Second option, in case you don't want to await for the result is to use Task.start_link. This is ok, but again as name of start function is, the tasks process is linked to yours, so crashes in any of those two will cause other to crash too.

Third option, is to use Task.Supervisor, just open your app ex file (probably one which has name as you project and it is in lib folder) and under children = [... list add at the bottom like this

children = [
    ...
    supervisor(Task.Supervisor,[], [name: MyApp.TaskSupervisor])
]

this will start supervisor process in your app with name MyApp.TaskSupervisor which than you can call and tell what code to run and supervise it.

Now this third option gives you a bit more control in your app since:

  1. You can run task async (in background) without linking process which is instructing supervisor what task should do and task process, instead supervisor will monitor this task
  2. you can still await for the result
  3. you can still link task if you want your process to crash if task crashes
  4. You can event distribute task into other nodes easily.

You can find more info about this in documentation

0
votes

I was having the exact same question, so I decided to give a concrete answer that is as simple as possible.

  1. Add a dynamic supervisor to your /lib/application.ex. See the documentation here.
def start(_type, _args) do
  # List all child processes to be supervised
  children = [
  # ...
  # add the following line
  {Task.Supervisor, name: MyApp.TaskSupervisor}
  ]
  # ...
  1. Run any kind of code from anywhere like a controller or context. https://hexdocs.pm/elixir/Task.Supervisor.html#start_child/3
Task.Supervisor.start_child(MyApp.TaskSupervisor, fn ->
  IO.puts "I am running in a task"
end)

Note that in this case you don't have to await your task. That also means that error handling is up to you. You can apply options (see previous link) when you execute your code. The default :temporary will not restart the task upon failure which has the advantage that it will not crash it's parent (the application) with repeated failures.


Now to your specific question assuming that you don't care about the result of the question building. I'd implement it in the controller like this:

def build_question(conn, _params) do
  Task.Supervisor.start_child(MyApp.TaskSupervisor, fn ->
    MyApp.Questions.create_total_points_question(conn)
  end)
  conn
  |> put_flash(:info, "Question is currently being processed.")
  |> redirect(to: page_path(conn, :index))
end

If you care about the result, then this is not the way. In that case I'd recommend a liveview. It can either:

  • Just listen to the event, execute the action and return the result as soon as it's ready (in the meantime blocking user action, even though he sees the page). I don't recommend that.
  • Execute a background task and be notified by an Endpoint. That is much easier than it sounds. You basically subscribe when you mount your liveview and make sure your background job publishes to the endpoint.
  • Have your async Task send you a message upon completion. In that case you'd give it a pid to notify.
  • Maybe something else? I'm pretty sure this has been answered somewhere else anyways.