6 tips to ensure your rake task runs smoothly

ยท

4 min read

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

  1. The assumption of the column value is as your expectation?

  2. What will happen if the assumption isn't true?

  3. How do you know when something went wrong?

  4. 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 and end_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

ย