I wrote a small Rails app, which sends emails on a particular schedule. For example to send an email every week on monday and thursday at 7:00am Melbourne time.
However Heroku doesn’t have cron or good free alternative. I wrote a cron replacement in 5 minutes. This post shows how I did it. There are three parts:
Cron
We don’t want to reinvent the wheel and want to use the standard cron format to express a schedule:
cron = '00 7 * * mon,thu Australia/Melbourne'
Now we have the cron format, we need to parse it. I used the rufus-scheduler
gem:
scheduler = Rufus::Scheduler.parse(cron)
The scheduler
object knows the previous and the next cron times, as well its frequency. We’ll get to the scheduler later.
Storage
When the cron executes we need to remember and store it somewhere. Heroku does not keep the state but any key-value storage would work. I used the simple Rails model with name
and time
fields called Activity
.
create_table :activities do |t|
t.string :name
t.timestamps
end
class Activity < ApplicationRecord; end
Trigger
For the final part we need a trigger to run our cron scheduler. A real cron runs every minute. Luckily Heroku has the free Heroku Scheduler. It can trigger the cron every 10 minutes. That means the best precision we can get is 10 minutes. Not bad for a free solution. If you need more accuracy, don’t be cheap and do it properly.
Now we have all of the bits ready and it’s time to hook them up. We will put it inside a rake task so Heroku Scheduler can run it.
task cron: :environment do
cron = '00 7 * * mon,thu Australia/Melbourne'
scheduler = Rufus::Scheduler.parse(cron)
name = 'news-email'
time = scheduler.previous_time.utc
activity = Activity.find_by(name: name, created_at: time)
if activity.nil?
Activity.create!(name: name, created_at: time)
Email.send
end
end
The code is done, only the heroku addon config is left:
And now our cron is ready. Hooray!
So how does it work?
After the cron notation is parsed, we ask the scheduler what is the previous time for it. Let’s say today is tuesday. Then the previous time would be yesterday at 7am (ignoring timezones)
Then we check if it executed yesterday by checking the existence of the Activity. If it didn’t, we create the Activity record to mark the the execution and send email.
The next time the rake task runs it will have the same previous time for the cron until the next cron schedule occurrence. Then the process will repeat again.
Note: The example is simple in this post, but it’s easy to expand it to support multiple cron tasks or different storages: Redis, Mongo, etc.