Những điều bạn cần biết để tạo một Rake Task
Bài đăng này đã không được cập nhật trong 6 năm
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 methodall
- 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