+1

Những điều bạn cần biết để tạo một Rake Task

Rake task là một phần rất quan trọng trong Rails Apps của chúng ta, bởi vì chúng ta thường xuyên maintainance hoặc chạy các job về data migration với một số lượng data rất lớn. Đã từng có member của mình hỏi rằng "Điều gì mà mình nên nắm rõ khi viết rake task và làm thế nào để biết rằng mình đang viết 1 rake task chuẩn form"

Để trả lời cho câu hỏi này không đơn giản, bởi vì nó còn phụ thuộc vào thời gian bạn cần để hoàn thành nó ra sao. Khi cần gấp thì thật khó để viết được một task tối ưu được. Tuy nhiên, mình sẽ chỉ cho các bạn một số rule cơ bản khi viết rake task của mình. Hy vọng rằng nó sẽ giúp ích được cho các bạn nhiều.

Làm thế nào để tạo ra 1 rake task là 1 good task?

Theo mình thì 1 rake task được cho là tốt, dễ hiểu v..v.. khi và chỉ khi:

  • Nó có description đơn giản và dễ hiểu
  • Khi rake task sử dụng namespace để group những task có liên quan với nhau
  • File structure follow theo namespace structure
  • Trong rake task các thầnh được chia ra theo từng class và có thể reuse hoặc test chúng 1 cách dễ dàng
  • Phải có log file bao gồm start date, end date, và lỗi v..v.

Cách viết một description có ý nghĩa

Bad

# lib/tasks/import_topics.rake
task import_topics: :environment do
  ...
end

Viết một description rất quan trọng bởi vì sẽ có những thời điểm người khác đọc task của bạn và đôi khi họ không hiểu được bạn đang làm gì và cần có 1 sự chỉ dẫn. Hay đôi khi chính bản thân bạn quay lại đọc task của mình sau một thời gian dài không động đến nó. Ngoài ra, không cần đọc code mà chỉ cần đọc description bạn cũng sẽ hiểu được là cái này đang làm về cái gì. Ngoài ra cũng rất hữu hiệu khi bạn muốn list ra toàn bộ các task đang avalaible với việc sử dụng rake -T. Hiện nay chúng ta chỉ quan tâm rằng task này quan trọng và cần làm ngay, đôi khi quên mất những điều rất nhỏ như viết description về thứ mà bạn định làm

Thêm vào một đoan description ngắn gọn phía trước task ví dụ như "Migrate topics from legacy database to new database", nó sẽ cho bạn biết rõ hơn vê việc import dữ liện từ legacy database sang database hiện tại của bạn.

Good

# lib/tasks/migrate_topics.rake
desc 'Migrate topics from legacy database to new database'
task migrate_topics: :environment do
  ...
end

Note: Nếu bạn không thể giải thích rake task của bạn chỉ trong 1 dòng, thì điều đó có nghĩa là rake task của bạn cần phải làm chắc chắn nhiều hơn 1 job và điều đó nghĩa là bạn cần cân nhắc đến việc chia nhỏ rake task của mình.

Nhóm task của bạn bằng cách sử dụng Namespaces

Bad

# lib/tasks/migrate_topics.rake
desc 'Migrate topics from legacy database to new database'
task migrate_topics: :environment do
  ...
end
# lib/tasks/migrate_users.rake
desc 'Migrate users from legacy database to new database'
task migrate_users: :environment do
  ...
end
desc 'Migrate questions from legacy database to new database'
task migrate_questions: :environment do
 ...
end

Rất đơn giản để nhận ra rằng tất cả các task này dùng để sử dụng migrate information. Sẽ dễ dàng hơn nếu chúng ta nhóm chúng lại thành 1 nhóm cùng chung namespace migrate

Good

# lib/tasks/migrate/topics.rake
namespace :migrate do
 desc 'Migrate topics from legacy database to new database'
 task topics: :environment do
    ...
 end
end
# lib/tasks/migrate/users.rake
namespace :migrate do
  desc 'Migrate users from legacy database to new database'
  task users: :environment do
    ...
  end
end
# lib/tasks/migrate/questions.rake
namespace :migrate do
  desc 'Migrate questions from legacy database to new database'
  task questions: :environment do
    ...
  end
end

Bây giờ chúng ta có thể thấy rằng các rake task đều có sự liên quan, sử dụng namespaces sẽ giúp code của bạn có sự thống nhất, dễ hiểu và "clean"

Rake File Structure

Bad

# lib/tasks/migrate_topics.rake
desc 'Migrate topics from legacy database to new database'
task migrate_topics: :environment do
   ...
end
# lib/tasks/migrate_users.rake
desc 'Migrate users from legacy database to new database'
task migrate_users: :environment do
   ...
end
# lib/tasks/migrate_questions.rake
desc 'Migrate questions from legacy database to new database'
task migrate_questions: :environment do
   ...
end

File Structure

lib
 └── tasks
     ├── recalculate_badges_for_users.rake
     ├── migrate_users.rake
     ├── migrate_topics.rake
     ├── migrate_questions.rake
     ├── migrate_answers.rake
     ├── recalculate_best_answer.rake
     ├── topic_accessible_by_url.rake
     ├── invalid_questions.rake
     ├── remove_duplicated_topics.rake
     ├── calculate_last_activity_for_question.rake
     ├── ...
     ├── clean_votes.rake
     └── cache_visits.rake

Mặc dù tất cả các file name để có những đầu mục theo từng loặc, nhưng nếu bạn có 30 hoặc nhiều hơn nữa số lượng rake task cùng chung 1 folder thì điều đó sẽ làm bạn rất tốn thời gian trong việc tìm kiếm

Mỗi một rake task sẽ đảm nhận một nhiệm vụ trên 1 resource nào đó, hoặc làm việc với nhiều resource (nhưng sẽ luôn hoạt đông xoay quanh 1 resource chính). Do đó chúng ta xác định resource chính của rake task rồi nhóm chúng vào cùng với nhau trong cùng một folder và namespaces, sẽ giúp cho việc quản lý và maintainance tốt hơn rất nhiều

Good

# lib/tasks/migrate/topics.rake
namespace :migrate do
  desc 'Migrate topics from legacy database to new database'
  task topics: :environment do
     ...
  end
> end
namespace :migrate do
  desc 'Migrate users from legacy database to new database'
  task users: :environment do
     ...
  end
end
# lib/tasks/migrate/questions.rak
# lib/tasks/migrate/users.rake
namespace :migrate do
  desc 'Migrate questions from legacy database to new database'
  task questions: :environment do
     ...
  end
end

Main resource là migration do đó chúng ta sẽ sử dụng migrate là namespace và tạo một folder tên giống với tên của namespace đó. Cứ như vậy chúng ta làm với các task còn lại

File structure:

Sau khi chúng ta refactor lại thì file structure của chúng ta sẽ rõ ràng và gọn gàng hơn rất nhiều

lib
 └── tasks
     ├── migrate
     │   ├── users.rake
     │   ├── topics.rake
     │   ├── questions.rake
     │   └── answers.rake
     ├── users
     │   ├── recalculate_badges.rake
     │   └── cache_visits.rake
     ├── ...
     ├── questions
     │   ├── recalculate_best_answer.rake
     │   ├── topic_accessible_by_url.rake
     │   ├── clean_votes.rake
     │   ├── log_invalid.rb
     │   └── calculate_last_activity.rake
     └── topics
         └── remove_duplicated.rb

Isolating task của bạn bằng cách sử dụng class

Mình sẽ bắt đầu phần này bằng một ví dụ, bởi vì nó sẽ giúp bạn hiểu rõ hơn về phần này.

Chúng ta sẽ có một app dạng StackOverFlow, user của chúng ta có thể đặt câu hỏi, trả lời chúng, leave comment, và do đó chúng ta có rất nhiều dữ liệu trong database. Và bây giờ chúng ta quyết định thêm 1 chức năng Huy hiệu cho hệ thống của mình.

Bây giờ chúng ta sẽ phải develope hệ thống huy hiệu của hệ thống, chúng ta chỉ cần tính tính lại toàn bộ huy hiện mà người dùng đã sử dụng. Và chúng ta cần đến rake task để xử lý việc này.

Bad

# lib/tasks/users/recalculate_badges.rake
namespace :users do
  desc 'Recalculates Badges for All Users'
  task recalculate_badges: :environment do
    User.find_each do |user|

      # Grants teacher badge
      if user.answers.with_votes_count_greater_than(5).count >= 1
        user.grant_badge('teacher')
      end

      ...

      # Grants favorite question badge
      user.questions.find_each do |question|
        if question.followers_count >= 25
          user.grant_badge('favorite question') && break
        end
      end

      # Grants stellar question badge
      user.questions.find_each do |question|
        if question.followers_count >= 100
          user.grant_badge('stellar question') && break
        end
      end

    end
  end
end

Task nhìn qua khá dễ hiểu và đơn giản, nhưng chúng có một số vấn đề:

  • Quá khó test
  • Có quá nhiều logic và chúng không isolated (rõ ràng, rành mạch)
  • Có quá nhiều đoạn bị lặp
  • Task này quá lớn, và đôi khi khá khó đọc trong trường hợp bạn có 25 dạng huy hiệu và mỗi một điều kiện cho một huy hiệu, với một phép tính đơn gianr thì chúng ta cần đến 150 lines

Chúng ta đã chỉ ra những vấn đề trong task này, và bây giờ cùng nhau bắt tay vào sửa chúng. Chúng ta cần biết chính xác logic và chuyển chúng vào Service object

Good

# lib/tasks/users/recalculate_badges.rake
namespace :users do
  desc 'Recalculates Badges for All Users'
  task recalculate_badges: :environment do
    User.find_each do |user|

      RecalculateBadges.new(user).all

    end
  end
end
# app/services/recalculate_badges.rb
class RecalculateBadges

  attr_reader :user, :questions, :answers

  def initialize(user)
    @user      = user
    @questions = user.questions
    @answers   = user.answers
  end

  def all
    teacher
    favorite_question
    stellar_question
  end

  def teacher
    ...
    grant_badge('teacher')
  end

  def favorite_question
    question_followers_count_badge(25, 'favorite question')
  end

  def stellar_question
    question_followers_count_badge(100, 'stellar question')
  end

  private

    def grant_badge(badge_name)
      return unless badge_name
      user.grant_badge(badge_name)
    end

    def question_followers_count_badge(followers_count, badge_name)
      ...
      grant(badge_name)
    end

end

Chúng ta cùng nhau phân tích lại một chúng về những thay đổi và những lợi ích kèm theo nó.

  • Phần logic trong rake task của bạn dễ đọc và dễ hiểu hơn rất nhiều, tất cả các method trong class RecalculateBadges được biểu diễn trong một method all
  • Chúng ta có thể test logic của phần huy hiệu này dễ dàng hơn rất nhiều do được phân chia rất rõ ràng theo từng phần.
  • Chúng ta đã loại bỏ rất nhiều phần duplication, refactor code, DRY. Có một số điều quan trọng mà mình muốn nhấn mạnh trong ví dụ vừa rồi
  • Với Service Objects, chúng ta t hườn sử dụng instance methods thay vì class methods, sẽ dễ dàng hơn nhiều trong việc refactor
  • Chú ý rằng Service Object chỉ thực hiện hành động trên một đối tượng duy nhất không phải trong collection, điều đó sẽ giúp chúng ta linh hoạt hơn trong việc reuse class này trong các phần khác của application

Hiển thị tiến độ công việc mà không bị chi tiết quá

Có một điều khiến tôi cảm thấy rất khó chịu đó là việc hiển thị thông tin không liên quan khi chạy 1 task. Điều đó làm bạn cảm giác khó khăn trong việc giám sát tiến trình hoạn động của bạn và làm cho terminal của bạn cảm giác như một bãi rác vậy. Quá nhiều thông tin gây nhiễu loạn, nhưng những thông tin cần thiết lại bị lẫn vào trong những thông tin đó

Bad

# lib/tasks/users/recalculate_badges.rake
namespace :users do
  desc 'Recalculates Badges for All Users'
  task recalculate_badges: :environment do
    User.find_each do |user|

      puts "#{user.first_name} #{user.last_name} - #{user.email}"
      RecalculateBadges.new(user).all

    end
  end
end

Terminal Output:

Mario Krols - mkrols@gmail.com
Kristen Delt - kdelt@gmail.com
Monica Lewinsky - mlewinsky@clinton.com
...
Fake User - fuser@outlook.com

Task này bạn cần hiện những thông tin detail cần thiết nhưng tránh những thông tin thừa thãi, gây nhiễu, vậy bạn cần xem những gì:

  • Số lượng người dùng đang được xử lý và bao nhiêu đã được xử lý
  • Email của người dùng đã được xử lý, bạn có thể xác minh chúng bằng các thuộc tính mang tính duy nhất như id
  • Một thông báo chi tiết trong trường hợp thất bại

Hãy nhớ rằng chỉ thông báo những thông tin mà bạn cần, tất cả những thứ khác chỉ làm cho mọi thứ thêm rối và làm cho bạn cảm thấy khó đọc và khó nắm bắt thông tin mà bạn hiển thị ra.

Note: Nếu bạn không thích hiển thị các loại thông tin, thì có một loại output rất hữu dụng đó là hiển thị chấm . xanh khi đã hoàn thành và X đỏ khi không thành công. Rất dễ nhìn và gọn gàng để bạn có thể nắm bắt được tình hình các task đang chạy của mình.

.................X...........X.......XX........X.

Luôn luôn sử dụng Log file

Thử tưởng tượng app của bạn có khoảng 100.000 users và chúng ta cần tính toán số lượng huy hiện cho tất cả số lượng user đó

Bad

# lib/tasks/users/recalculate_badges.rake
namespace :users do
  desc 'Recalculates Badges for All Users'
  task recalculate_badges: :environment do
    users_count = User.count

    User.find_each.with_index do |user, index|
      recaulculate_badges = RecalculateBadges.new(user)

      if recalculate_badges.all
        puts "#{index}/#{users_count} - #{user.email}".green
      else
        puts "#{index}/#{users_count} - #{user.email} - #{recalculate_badges.errors}".red
      end

    end
  end
end

Vấn đề chính là việc bạn sẽ phải ngồi cuộn chuột 1 cách dài đằng đẵng để có thể check được tiến độ của 100.000 users, điều đó rất rủi ro khi bạn bỏ lỡ mất điều gì bất thường hay lỗi mà mình không kịp nhìn hay phát hiện ra trong lúc mất tập trung chẳng hạn. Hoặc với số lượng lớn như thế cần đến hàng giờ chạy thì bạn không thể ngồi nhìn hàng tiến mà không mất đi sự tập trung của mình. Đôi khi việc chạy rake task chỉ là chạy background hoặc remote trong khi bạn làm việc khác. Do đó, việc sử dụng log file là điều BẮT BUỘC khi mà chúng ta viết rake tasks. Chúng sẽ giúp bạn quản lí được tiến độ của mọi task, có thể kiểm tra lại nếu có vấn đề hoặc có thể share cho mọi người một cách dễ dàng.

Good

# lib/tasks/users/recalculate_badges.rake
namespace :users do
  desc 'Recalculates Badges for All Users'
  task recalculate_badges: :environment do
    log = ActiveSupport::Logger.new('log/users_recalculate_badges.log')
    start_time = Time.now
    users_count = User.count

    log.info "Task started at #{start_time}"

    User.find_each.with_index do |user, index|
      recaulculate_badges = RecalculateBadges.new(user)

      if recalculate_badges.all
        log.info "#{index}/#{users_count} - #{user.email}"
      else
        log.info "#{index}/#{users_count} - #{user.email} - #{recalculate_badges.errors}"
      end

    end

    end_time = Time.now
    duration = (start_time - end_time) / 1.minute
    log.info "Task finished at #{end_time} and last #{duration} minutes."
    log.close
  end
end

Tracking start time, end time và khoảng thoài gian chạy là một điều rất quan trọng. Nếu bạn có 1 rake task và bạn chạy chúng hàng mỗi giờ, và rake task của bạn cần 1 hoặc nửa giờ để hoàn thành, task của bạn có thể bị trùng lặp với task đang chạy hoặc là bị kết thúc do tràn bộ nhớ của server chẳng hạn.

Note: Ngoài việc sử dụng log file, bạn nên in ra output, đó là cách để bạn chắc chắn rằng rake của bạn đang chạy,

Tổng kết

Việc sử dụng rake task rất quan trọng trong việc maintanance hệ thống, đặc biệt là với các hệ thống lớn đang hoạt động, việc chạy sai có thể ảnh hưởng rất lớn đến người dùng đang sử dụng dịch vụ đó. Do đó việc viết rake task kèm theo nhưng lời khuyên phía trên của mình là một trong những cách để bạn có thể đề phòng và tránh được những rủi ro không đáng có. Hy vọng rằng bài viết này của mình sẽ giúp ích cho các bạn.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.