Design ideas for managing thousands of notifications that must be run at specific times

I have a db table that has a table that holds notifications. Each notification has a start_time column that specifies when the notification should be triggered.

e.g. 9:15am

So my current thinking is to have some kind of a process that ticks every x seconds, loops through the notifications collection and if the start_time has been reached, add that notification to the ignore list and fire off a process to send out the notification.

I’ve actually implemented this in go but wondering how something like this could be handled using elixir.

Update
Notifications start_time is at the minute level, so 9:15am or 9:17am (seconds are ignored). If the timing by e.g. 10 seconds or so that is reasonable in this use case.

Are you planning to run this on a VM in a public cloud? Note that your program can be suspended for >1 second if so.

Ah, it seems like it wouldn’t affect you since you are not comparing the timestamps for equality.

I’ve actually implemented this in go but wondering how something like this could be handled using elixir.

Probably the same … Consider creating a process which would hold the “ignore list” and send :maybe_notify messages to itself every second.

defmodule Notifier do
  use GenServer

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: opts[:name])
  end

  def init(_opts) do
    send(self(), :maybe_notify)
    sent = []
    {:ok, sent}
  end

  def handle_info(:maybe_notify, sent) do
    # fetch notifications with now + 1 minute >= timestamp >= now - 1 minute
    # sned out notifications, collect confirmations, update `sent`
    Process.send_after(self(), :maybe_notify, 1000)
    {:noreply, sent}
  end

  def handle_info(:refresh_sent, _sent) do
    # probably should be done ~ every 24h
    {:noreply, []}
  end
end
1 Like

How many notifications are we talking about ?

You have several choices:

  1. Call Process.send_after for each notification to message a process to send that specific notification out.
  2. Use :timer:send_interval/3 to message a process to scan and spawn notifications to be sent out.
  3. Use :timer:apply_interval/4 to call a function which would scan and send out the notifications.
  4. Dump the notifications onto disc and let cron do the work.
  5. Use quantum

If you decide to spawn and scan table and you have lots of notifications consider having a timer and a table per hour. That’s 24 timers which would reduce the scan time since every notification outside that time period would immediately be excluded.

3 Likes

I don’t have # of notifications at this time, but I would like this to be a general purpose solution as I might have a few use cases for it. This one in particular might be a few thousand notifications. I’m currently using a fixed # of workers to process the notification.

What type of column are you storing the time in? Are you storing the timezone as well? Have you thought about how to handle daylight savings time transitions and other time weirdness.

This is the kind of work that a priority queue is made for: This way, rather than checking the whole list again at every tick, you can sleep until:

  • The time for the first one to be sent arrives.
  • A new scheduled notification is added to the system, in which case the process can re-set its timer.
  • Of course, if you have a lot of notifications (more than you can fit in the memory of one process), then it might make sense to do one of the following:
    • Have multiple ‘notification sender workers’, each with their own priority queues.
    • Have a separate layer where notifications are stored in a datastore and periodically the to-soon-be-sent notifications are fetched from there, and added to a process with a priority queue.

I think that is the closest you can get to having a system that sends the notifications ‘on time’ (strictly speaking, timers can always trigger a little later, but never earlier than the given time, based on the load of the system), as well has having still a small memory and execution overhead (which would not be the case when querying all notifications every minute, or calling functions even though there are no notifications to be scheduled.

4 Likes

So each handle_xxx can be set in a timer to trigger every x minutes?

Not sure what you mean by “set” in this context. But the process can be sent messages every x minutes which would be handled by handle_info(message, state).