+2

Cron job with gem whenever

1. Cron job là gì:

  • Đôi khi chúng ta cần implement các job cần được thực hiện theo 1 schedule cho trước, các job này được gọi chung là cron job.
  • Các job này có thể là gửi remind email cho user hằng ngày, gọi API crawl data của third party để import hoặc update data của app mỗi tuần 1 lần, xóa cache, xóa log, backup database, ...
  • Trong Rails chúng ta có thể sử dụng gem whenever để lên schedule cho cronjob.

2. Tạo demo app:

  • Chạy các command sau để tạo 1 app đơn giản.
    rails new demo_whenever_revise
    
  • Chạy các command migration cho user.
    rails g model user email name
    rails db:migrate
    
  • Thêm gem ffaker vào Gemfile.
    # Gemfile
    gem "ffaker"
    
  • Chạy command bundle install để install gem ffaker.
    bundle install
    
  • Tạo seed data cho user.
    # db/seeds.rb
    10.times do
      User.create email: FFaker::Internet.email, name: FFaker::Name.name
    end
    
  • Chạy command rails db:seed để tạo seed data.
    rails db:seed
    
  • Tạo rake task tạo random user.
    # lib/tasks/user/create_random.rake
    namespace :user do
      desc "Create random user"
    
      task create_random: :environment do
        puts "RUNNING rake user:create_random"
        user = User.create email: FFaker::Internet.email, name: FFaker::Name.name
        puts "CREATED user: #{user.inspect}"
      end
    end
    
  • Chạy command rake user:create_random để test rake task vừa tạo.
    rake user:create_random
    
  • Tiếp theo chúng ta sẽ dùng gem whenever để tạo cronjob cho rake task này. Let's go!

3. Gem whenever:

a. Install:

  • Thêm gem whenever vào Gemfile.
    # Gemfile
    gem "whenever"
    
  • Chạy command bundle install để install gem whenever.
    bundle install
    
  • Chạy command wheneverize để generate file config/schedule.rb.
    wheneverize
    

b. Update file config/schedule.rb:

  • Update lại file config/schedule.rb như sau.
    # config/schedule.rb
    every 1.minutes do
      rake "user:create_random"
    end
    
  • Mỗi lần update file config/schedule.rb ta cần chạy command whenever --update-crontab để update lại crontab.
    whenever --update-crontab
    
  • Chạy lại command whenever để check lại crontab.
    whenever
    
  • Check lại outout của crontab ta được kết quả như sau.
    * * * * * /bin/bash -l -c 'cd /home/hcm-102-0003/Desktop/demo_whenever_revise && RAILS_ENV=production bundle exec rake user:create_random --silent'
    
    ## [message] Above is your schedule file converted to cron syntax; your crontab file was not updated.
    ## [message] Run `whenever --help' for more options.
    
  • Ta thấy RAILS_ENV đang được set giá trị là production, có nghĩa là crontab này chỉ được chạy ở môi trường production, không được chạy ở môi trường development hay staging.
  • Nguyên nhân là môi trường chạy crontab được lưu trong biến @environment và biến này có giá trọ mặc định là production.
  • Ta có thể check lại bằng cách update lại file config/schedule.rb, in ra màn hình giá trị của biến @environment.
    # config/schedule.rb
    puts "Crontab is run on #{@environment}"
    
  • Sau đó chạy lại command whenever --update-crontab
    whenever --update-crontab
    
  • Output
    Crontab is run on production
    [write] crontab file updated
    
  • Ta có thể set giá trị của biến @environment bằng cách thêm đoạn code sau vào đầu file config/schedule.rb
    # config/schedule.rb
    set :environment, :development
    
  • Sau đó chạy lại command whenever --update-crontab
    whenever --update-crontab
    
  • Output
    Crontab is run on development
    [write] crontab file updated
    
  • Tuy nhiên ta đang hard code giá trị của biến @environment, ta cần phải set giá trị của biến @environment thay đổi theo môi trường.
  • Để làm vậy ta cần sử dụng Rails.env, update lại file config/schedule.rb như sau
    # config/schedule.rb
    require_relative "environment"
    set :environment, Rails.env
    
  • Chúng ta cũng có thể quy định outout log của crontab bằng cách thêm đoạn code sau vào file config/schedule.rb
    # config/schedule.rb
    set :output, "log/cron_job.log"
    
  • File config/schedule.rb hoàn chỉnh
    # config/schedule.rb
    require_relative "environment"
    
    set :environment, Rails.env
    set :output, "log/cron_job.log"
    
    # puts "Crontab is run on #{@environment}"
    # puts "Crontab is logged on #{@output}"
    
    every 1.minutes do
      rake "user:create_random"
    end
    
  • Chạy command để update crontab và check lại
    whenever --update-crontab
    whenever
    
  • Output
    * * * * * /bin/bash -l -c 'cd /home/hcm-102-0003/Desktop/demo_whenever_revise && RAILS_ENV=development bundle exec rake user:create_random --silent >> log/cron_job.log 2>&1'
    
    ## [message] Above is your schedule file converted to cron syntax; your crontab file was not updated.
    ## [message] Run `whenever --help' for more options.
    
  • Bạn có thể check lại kết quả bằng cách mở file log/cron_job.log và xem lại log, dưới đây là log của mình
    RUNNING rake user:create_random
    CREATED user: #<User id: 12, email: "deanna.braun@brekke.ca", name: "Claretha Hintz", created_at: "2019-06-26 08:58:03", updated_at: "2019-06-26 08:58:03">
    RUNNING rake user:create_random
    CREATED user: #<User id: 13, email: "kathyrn.kris@king.name", name: "Debera Purdy", created_at: "2019-06-26 08:59:03", updated_at: "2019-06-26 08:59:03">
    RUNNING rake user:create_random
    CREATED user: #<User id: 14, email: "leontine@russel.biz", name: "Erminia Weber", created_at: "2019-06-26 09:00:03", updated_at: "2019-06-26 09:00:03">
    

c. Jobtypes:

  • Mặc định whenever cung cấp cho chúng ta 4 loại jobtype để chạy các crontab khác nhau.

    1. command: chạy các command của linux hoặc rails app, ví dụ

      command "echo 'example command'"
      
    2. rake: chạy các rake task của rails app, crontab trong bài viết này cũng có jobtype là rake

      rake "user:create_random"
      
    3. runner: chạy các class method, trong source code tham khảo có bổ sung thêm 1 crontab có jobtype là runner, bạn có thể tham khảo thêm

      runner "User.create_random"
      
    4. script: chạy script file, ví dụ

      script "example_script"
      
  • Bạn cũng có thể tự define thêm jobtype mới, tham khảo document của gem whenever.

d. Date and time:

i. Gem Chronic:

  • Gem whenever đang sử dụng gem Chronic để parse date time.
  • Theo mặc định Chronic sử dụng thời gian theo option 12h và có hậu tố am hoặc pm để phân biệt.
  • Bạn có thể sử dụng thời gian theo option 24h và không cần sử dụng hậu tố am hoặc pm để phân biệt bằng cách set chronic_options như sau.
    set :chronic_options, hours24: true
    
  • Khi đó thay vì viết 3:00 am bạn có thể viết ngắn gọn là 3:00 hoặc 15:00 thay cho 3:00 pm.
  • Các option date time trong whenever bao gồm
    every n.minutes # or every :minute with n = 1 
    every n.hours   # or every :hour with n = 1
    every n.days    # or every :day with n = 1
    every n.months  # or every :month with n = 1
    every n.years   # or every :year with n = 1
    
    # hours in a day
    every "1:00am" 
    ...
    every "11:00am"
    
    # days in a week
    every :monday
    ...
    every :sunday
    
    # days in a month
    every "1st" 
    ...
    every "31st"
    
    # months in a year
    every :jan
    ...
    every :dec
    
  • Bạn cũng có thể kết hợp các option để tạo ra các option phức tạp hơn.
    every :day, at: "3:00am"
    every :month, at: "1st"
    every :year, at: :jan
    ...
    

ii. Timezone:

  • Hãy thử tưởng tượng Timezone Rails app của bạn là Tokyo UTC +09:00 nhưng server của bạn lại được deploy và set Timezone là UTC UTC +00:00.
  • Khi đó crontab 11:00 am của bạn sẽ được chạy lúc 11:00 am của Timzone nào nhỉ.
  • Expectation của chúng ta là 11:00 am của timezone Tokyo UTC +09:00 .
  • Nhưng sự thật là 11:00 am của timezone UTC UTC +00:00, tức là 08:00 pm của timezone Tokyo UTC +09:00.
  • Để tránh tình trạng này chúng ta có 2 solution.
    Solution 1: set timezone của server deploy trùng với timezone của app
    
  • Với solution này thì chúng ta không cần implement thêm code.
    Solution 2: convert timezone của rails app sang timezone của server
    
  • Với solution này thì chúng ta cần implement thêm hàm convert time của app sang timezone của server
    def server_timezone time
      Time.zone.parse(time).localtime
    end
    
    every :day, at: server_timezone("11:00 am") do
      command "echo 'example command'"
    end
    
  • Bạn có thểm tham khảo thêm ở issue#239.

e. Các vấn đề khác:

i. Deploy:

  • Nếu bạn sử dụng capistrano, bạn có thể tham khảo document của whenever.
  • Nếu bạn sử dụng mina, bạn có thể tham khảo thêm gem mina-whenever.
  • Nếu bạn sử dụng heroku, bạn có thể tham khảo thêm add-ons Heroku Scheduler.

ii. Test:

4. Document:

  • Source code demo mình đang để ở github, các bạn có thể clone về để tham khảo.
  • Document của whenever.
  • Bài viết là những kinh nghiệm mình có được khi tìm hiểu và sử dụng gem whenever khi làm dự án thực tế, mong đươc các bạn đóng góp thêm ý kiến để mình hoàn thiện thêm bái viết này.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí