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.