Scheduled function - Task or Agent?

Hey guys,

I’ve built a little marketplace app to try and get to grips with Elixir and Phoenix. Part of the app includes a 1-1 messaging system based on Channels to allow users to enquire about the items for sale.

What would you guys propose as being the best way to schedule the firing of a function (about 15-30 minutes in the future), but with the ability to cancel it if needs be. The behaviour I’m aiming for is after a message is sent, an email notification for the other user is scheduled. If the recipient views the conversation (and thus message) before that scheduled task is carried out, it is cancelled and no notification is sent.

I’ve built a similar system before in another marketplace app using Meteor, although on that occasion I just cheated and used a package called meteor-synced-cron which did all the work for me.

Given what I know of Elixir already, I’m guessing I should be looking at using a Task or an Agent for this? Are there any drawbacks to using these, given that potentially every message sent spawns a process (let’s assume this, as it’s worse case scenario) that runs for 15-30 minutes?

I’d be interested in your feedback and thoughts.

Cheers, Jamie.

(Oh, and a very early version of the site is running here)

I think you’ll like Quantum, which handles cron-like functionality. Both with a named configuration containing tasks and, which is what you want in this case, adding/removing unnamed jobs.

2 Likes

In a tick based game, I needed a timer to send periodic events. But I wanted to be able to pause/resume the interval. So Maybe You can use this …

defmodule Whatever.Ticker do
  use GenServer
  require Logger
  @name __MODULE__
  
  defstruct ... tick_interval: 500, ticker_ref: :none
...
  def handle_cast(:start_timer, %{tick_interval: tick_interval, ticker_ref: ticker_ref} = state) 
    when ticker_ref == :none do
    Logger.debug "#{@name} started"
    
    {:ok, new_ticker_ref} = :timer.send_after(tick_interval, :tick)
    {:noreply, %{state | ticker_ref: new_ticker_ref}}
  end

  def handle_cast(:start_timer, state), do: {:noreply, state}

  def handle_cast(:stop_timer, %{ticker_ref: ticker_ref} = state) 
    when ticker_ref == :none do
    {:noreply, state}
  end

  def handle_cast(:stop_timer, %{ticker_ref: ticker_ref} = state) do
    Logger.debug "#{@name} cancelled"
    
    :timer.cancel(ticker_ref)
    {:noreply, %{state | ticker_ref: :none}}
  end
...
end

You store the ticker ref in state, then you can cancel it. I also stored interval, because I wanted to be able to accelerate the game after some time. I saw this here http://www1.erlang.org/examples/small_examples/index.html from Joe Armstrong, Tetris sample.

You can define the interval to 15 minutes.

I hope this help

3 Likes

Thanks for the great responses guys!

@Qqwy I’ll confess to looking at Quantum, and completely missing the named job functionality. I looked on the GitHub page and only saw the “every minute, every hour” type tasks. That’ll teach me!

@kokolegorille Equally, as per above, I hadn’t thought to look into Erlang libraries/functionality that could help me, so massive +1 for reminding me to think a little bigger! :timer looks like the piece of the puzzle I was missing when I was thinking about how I’d go about this.

Both a great help. Now I just try them both out! Cheers.

One problem that using :timer directly does not have an answer to, is what happens if the process that started the timer crashes before the timer expires. I believe this will mean that the timed event is just discarded. This might not be good enough for your application. As a side note, :timer is a lot slower than Process.send_after or :erlang.start_timer (source).

What Quantum does, if I remember correctly, is basically start a single GenServer process that uses send_after to send itself a message once every second. The handler for this then starts separate tasks to perform the different jobs, and finally runs send_after again to send the next request. This way, the job queue is separate from both the execution of the tasks and the location where a job is scheduled.

Ah, clever. I’ll have a play with it on the weekend then and see what I come up with!

With the number of questions on how to run a multitude of functions after a set delay and with all the answers being rather inefficient, I’m tempted to make a library for it, maybe called TaskAfter or something (since taking Task.After would snuggle in to Elixir’s pre-made namespace too much)… Would there be a demand for it?

6 Likes

@OvermindDL1 Yes, yes, yes!

cough

2 Likes

Big thumbs up from me @OvermindDL1. Thank you!

1 Like