6 tips to ensure your rake task runs smoothly
Rake is a task runner/task management tool in Ruby.
You can create diverse tasks, put tasks in the Rakefile
, and execute a command like rake :your_awesome_task
. Your task will start running by Rake.
In Ruby, you can put task code inside a file named Rakefile
, rakefile
, Rakefile.rb
or rakefile.rb
.
# rakefile.rb
desc 'Say hello'
task :say_hello do
puts 'Hello!'
end
# In your terminal
$ rake say_hello
#=> Hello!
In Rails, you can put task code under the lib/tasks
folder.
# lib/tasks/say_hello.rake
desc 'Say hello'
task say_hello: :environment do
user = User.first
puts "Hello, #{user.name}!"
end
# In your terminal
$ rake say_hello
#=> Hello, Lynn!
Rake is used for common administration tasks. Some commands you might be probably familiar with, such as rake db:migrate
, rake generate model
, rake routes
. (Take a look at more rake commands.)
Rake is useful when we need to update column values in the database. There are 6 tips I often use to ensure records can be updated smoothly when running a rake task.
Tips1: Ask yourself 4 questions
- The assumption of the column value is as your expectation?
- What will happen if the assumption isn't true?
- How do you know when something went wrong?
- How can you fix errors immediately when something went wrong?
These 4 questions can help us manage risks that might happen when running a task.
Tips2: Collect failed records list
- Use
!
(ex.save!
,update!
etc) to raise an exception if a validation error occurs. - Rescue the error, then you can collect the failed records and failed reason.
You can understand why, how many, and which records don't update successfully by collecting a failed records list. It's usable to fix failed cases.
namespace :user do
desc 'update name'
task update_name: :environment do
failed_list = []
User.find_each do |user|
user.update!(name: "best-#{user.name}")
rescue StandardError => e
failed_list.push({ user_id: user.id, reason: user.inspect })
Rails.logger.error "[task] user_id: #{user.id} failed to update user name."
Rails.logger.error "[task] user_id: #{user.id} failed reason: #{e.inspect}"
end
Rails.logger.info "[task] Failed list: #{failed_list}"
p "[task] Failed list: #{failed_list}"
end
end
Tips3: Record a task running duration
- Record
start_at
andend_at
timestamp.
Sometimes the system needs downtime when we run a task. However, downtime is expensive because it will impact the revenue of the company.
Before running a task on the production environment, running a task on the staging and recording a running duration can help us estimate how much time the task will spend. This will be a piece of important information to communicate with PMs or other departments.
namespace :user do
desc 'update name'
task update_name: :environment do
start_at = Time.zone.now
Rails.logger.info "[task] Start to update users name. Time: #{start_at}"
User.find_each do |user|
#...
end
end_at = Time.zone.now
duration = ((end_at - start_at) / 60.seconds).to_i
Rails.logger.info "[task] All of user records update completed. Time: #{end_at}"
p "[task] Start to update users name. Time: #{start_at}"
p "[task] All of user records update completed. Time: #{end_at}"
p "[task] Task running duration: #{duration} minutes"
end
end
Tips4: Make current progress visible
Print how many records you have updated now.
If you don't print anything when you run a task, your terminal will be very silent. Printing the log can help you keep track of the current progress. I believe this is an essential user experience for developers. 😂
namespace :user do
desc 'update name'
task update_name: :environment do
user_updated_count = 0
User.find_each do |user|
user.update!(name: "best-#{user.name}")
user_updated_count += 1
p "Current updated count => #{user_updated_count}"
end
Rails.logger.info "[task] Final: Update #{user_updated_count} user records."
p "[task] Final: Update #{user_updated_count} user records."
end
end
Tips5: Add a confirmation step to your task
Some tasks are destructive, so avoiding the fat-finger problem is crucial.
namespace :user do
desc 'replace_data'
task replace_data: [:environment, :confirm_to_replace_data] do
#...
end
end
desc 'confirm to replace data'
task :confirm_to_replace_data do
confirm_token = rand(36**6).to_s(36)
$stdout.puts "[WARNING!!] Please enter confirmation code if you confirm to replace user data: #{confirm_token}"
input = $stdin.gets.chomp
raise "Aborted! Confirmation code #{input} is invalid." unless input == confirm_token.to_s
Rails.logger.info "Confirm to replace. Time: #{Time.zone.now}"
end
Tips6: Write some test cases for your task
Last but not least, remember to write tests for your task. This can ensure your assumption is as you think.
Reference:



