Train on a rails

Ruby on Rails

Managing Cron Jobs with Mina and Whenever

Steven Yap   17 Nov , 2015  

In this post, we discuss about the purpose of using a deployment gem like Mina and demonstrate how easy it is to customise the deployment process using Whenever (a cron job manager gem) as an example.

What is Whenever

Whenever is a gem which makes our life easier to create, edit and delete cron jobs in our app. If you don’t know what is cron jobs, please read it here in Wikipedia.

Using Whenever gem, we can easily specify all our cron jobs like this:

every 3.hours do
runner "MyModel.some_process"
rake "my:rake:task"
command "/usr/bin/my_great_command"
end

every 1.day, :at => '4:30 am' do
runner "MyModel.task_to_run_at_four_thirty_in_the_morning"
end

every :hour do # Many shortcuts available: :hour, :day, :month, :year, :reboot
runner "SomeModel.ladeeda"
end

every :sunday, :at => '12pm' do # Use any day of the week or :weekend, :weekday
runner "Task.do_something_great"
end

every '0 0 27-31 * *' do
command "echo 'you can use raw cron syntax too'"
end

# run this task only on servers with the :app role in Capistrano
# see Capistrano roles section below
every :day, :at => '12:20am', :roles => [:app] do
rake "app_server:task"
end

This way of specify our cron jobs is not only easier to understand, it also declares the cron jobs that are needed in the app clearly. Since cron jobs are also part of the whole app, it is important that we capture them in the app code base and also in our git repository.

If we manually write the cron jobs in the production server, the cron jobs will be hidden from our day-to-day work which creates unknown performance overheads or even time-bombs in new server deployment.

Hence, we prefer using Whenever over manually writing the cron job in the production server.

To generate the cron jobs, you simply run bundle exec whenever --update-crontab.
To clear the cron jobs, you can run bundle exec whenever --clear-crontab.
It is that easy!

What is mina

Mina is an alternative deployment gem to Capistrano. It is much faster and easier to setup so we recommend it to many startups or web apps that do not require a complicated deployment process.

After you have bundled Mina and ran its installation, Mina will create a file at config/deploy.rb. Everything you need to know about how to deploy your app can be found there.

You can easily add custom tasks in your deployment like this:

# config/deploy.rb
task :down do
invoke :maintenance_on
invoke :restart
end

task :maintenance_on
queue 'touch maintenance.txt'
end

task :restart
queue 'sudo service restart apache'
end

Main Purpose of a Deployment Script

The purpose of using Mina (or any other deployment gems) is to let us have pain-free deployment to server whenever we have new codes.
We want to just type mina deploy and our deployment script pushes all the new code to server and deploy everything for us without us remembering server login password, after deployment tasks like changing file permissions, etc, etc, etc.

This is not only painless and joyful for developers, it is also a much more productive and stable deployment process.

To make this a reality, all the knowledge of setting up or deploying to the server for the app to run properly must be captured in the deployment script. In this case, it’s our Mina deployment script config/deploy.rb.

For the case of Whenever cron jobs, we want it to be such that every time we deploy to the server, Mina will automatically create the cron jobs using Whenever.
This will ensure that our cron jobs are always kept up to date and we will not ‘forget’ to update the cron jobs if we make any changes in Whenever.

This is happy deployment thinking. 🙂

Adding Whenever Task to Mina Deployment

To let Mina create our cron jobs through Whenever, we need to create a custom task in config/deploy.rb.

Let’s name this custom task :update_cron_jobs.

Why not name it :update_whenever_cron_jobs?
By naming our methods by their function (ie. what they do), it will be easier to re-use or modify in the future.
If we name the task as :update_whenever_cron_jobs, we have to change the name again if we use another gem instead of Whenever.
Through naming by function, we won’t have to change the task name even if we change the implementation.

Here’s the :update_cron_jobs task:

# Whenever tasks in config/deploy.rb
desc "Update cron jobs"
task :update_cron_jobs => :environment do
queue "bundle exec whenever --update-crontab --set 'path=#{app_root_directory}/current/'"
queue %[echo "-----> Updated Whenever cron jobs."]
end

There are some booby traps here:

1) We need to set the identifier to a unique name (generally we choose our app name).
This is important as the default behavior of Whenever is to use the current path which, in the case of Mina deployment, is unique every time we deployed.
Therefore, if you did not set the identifier, each time you invoke :update_cron_jobs, there will be duplicated cron jobs created in the server because Whenever cannot identify its previously generated cron jobs. (One of our production servers died this way… 🙁 )

Every time you deploy via Mina, Mina will push your code into a new folder /releases/ and then if the deployment is successful, Mina will symlink #{app_root_directory}/current to the new release folder.
A symlink is like a shortcut or pointer that points to another location.

2) The Whenever command line is actually executed from the /releases/ folder so we want to explicitly set the path for the cron jobs generated (--set 'path=#{app_root_directory}/current/').
This generates a more consistent and stable cron jobs.

3) Make sure you set the #{app_root_directory} correctly in your Mina deploy script.

Once we have the task, we can easily add it into the Mina deploy script.
Just add the line invoke :update_cron_jobs in your config/deploy.rb like below:

desc "Deploys the current version to the server."
task :deploy => :environment do
deploy do
invoke :'git:clone'
invoke :'deploy:link_shared_paths'
invoke :'bundle:install'
invoke :'rails:db_migrate'
invoke :'rails:assets_precompile'

invoke :update_cron_jobs

to :launch do
queue "mkdir -p #{deploy_to}/#{current_path}/tmp"
queue "touch #{deploy_to}/#{current_path}/tmp/restart.txt"
end
end
end

Now you can run mina deploy and the deployment script will run the :update_cron_jobs task to update your cron jobs automatically!
To double check that the cron jobs are created correctly, you can run crontab -l on your production server.

Conclusion

The purpose of using a deployment gem like Mina is to reduce our workload and potential mistakes in deployment. As shown in this post, it is very easy to add a custom task to Mina and make it execute automatically every time we deploy.

In building our app, we would want everything to be declared clearly and explicitly so that everyone’s life will be easier for maintenance, bug hunting and ramping up another coder. Whenever gem helps our coding practice in this cron jobs aspect.

Image source

, , , , , ,

One of the founders of Futureworkz - a Singapore-based mobile and web development company. He is a geeky Ruby coder and consults company on IT implementations and business startups.