Scheduling Notifications

Scenario: you wanna delivery users Notifications (e.g. email) at certain intervals. The system is run periodically and we need to work out whether to sent or not. Simples, right? Um…

We’re in python and using croniter package. We reuse the concepts of cron, which is the obvious parallel in linux. Let’s think of cron as just an interval maker:

|    <- window ->   |    <- window ->   |    <- window ->   |
^                   ^                   ^                   ^
action              action              ...                 ...
  
# cron format is:
minute hour day-of-month month day-of-week
# Some examples:
Once a minute:
* * * * * 
Once an hour (at 1 minute past the hour)
1 * * * *
Once a day (at 01h01 in the morning)
1 1 * * *
Once a month (at 00h00 on first day)
0 0 1 * *

Note all of these are regular instants in time. We define a ‘window’ as shown above. We call “now” the time the script is run. We define a sent_in_window() function which determines if a Notification has been (previously) sent in that the current window.

Let’s understand with a run through:

Initially, we have no sends:

Schedule:  |    <- window ->   |    <- window ->   |    <- window ->   |
              ^
              now
sent_in_window() == False

For the first run, either we (1) send immediately or (2) (better) send next interval. So if is_first() then use created_at date of Notifier as the ‘first’ point

Schedule:  |    <- window ->   |    <- window ->   |    <- window ->   | 
             ^        ^
        created_at   now                                                                 

Since sent_in_window() = True, we don’t send. Ok, that’s good.

Now later on:

Schedule:  |    <- window ->   |    <- window ->   |    <- window ->   |  
           ^                            ^
          created_at                   now


sent_in_window()==False  => do send

Now even later on:

Schedule:  |    <- window ->   |    <- window ->   |    <- window ->   |  
           ^                            ^         ^
          created_at                   sent       now

sent_in_window()==True  => don't send

And finally later on:

Schedule:  |    <- window ->   |    <- window ->   |    <- window ->   |  
           ^                            ^             ^
          created_at                   sent          now

sent_in_window()==False => do send

Edge cases and thoughts

We want to run pretty close to the scheduled time, so we should run often.

If we run very late in the interval, two notifications could be send almost simultaneously, one before and one after the instant requested. (Consider disallowing sends in last (arbitarily) half of window).

Another issue with late sends is that we probably shouldn’t use now() and a duration (e.g. 30d) to define the window. Instead, we should pass the required window (start,end) to the Notification and construct it based on that. For example, send a monthly email with charts on 05 Feb should display 01 Jan to 31 Jan (not 6 Jan to 5 Feb).

If pending, and we don’t send (e.g. later in the pipeline, something filters it out), that’s ok. The Notification will not be created and the next run will try to send it.

If we completely miss a window that’s ok too – we just won’t send a Notification in that window.

Implementation

.