Lệnh gọi lại ActiveRecord là một cách dễ dàng để chạy mã trong các giai đoạn khác nhau của vòng đời mô hình của bạn.
Ví dụ:giả sử bạn có một trang web Hỏi và Đáp và bạn muốn có thể tìm kiếm thông qua tất cả các câu hỏi. Mỗi khi bạn thực hiện thay đổi đối với một câu hỏi, bạn sẽ muốn lập chỉ mục câu hỏi đó trong một cái gì đó như ElasticSearch. Việc lập chỉ mục mất một chút thời gian và không khẩn cấp, vì vậy bạn sẽ thực hiện việc này trong nền với Sidekiq.
Đây có vẻ là thời điểm lý tưởng để sử dụng after_save
gọi lại! Vì vậy, trong mô hình của bạn, bạn sẽ viết một cái gì đó như:
class Question < ActiveRecord::Base
after_save :index_for_search
# ...
private
def index_for_search
QuestionIndexerJob.perform_later(self)
end
end
class QuestionIndexerJob < ActiveJob::Base
queue_as :default
def perform(question)
# ... index the question ...
end
end
Điều này hoạt động tuyệt vời! Hoặc, ít nhất, có vẻ như vậy. Cho đến khi bạn xếp hàng đợi nhiều công việc hơn và thấy những lỗi này hiển thị:
2015-03-10T05:29:02.881Z 52530 TID-oupf889w4 WARN: Error while trying to deserialize arguments: Couldn't find Question with 'id'=3
Chắc chắn, Sidekiq sẽ thử lại công việc và nó có thể sẽ hoạt động vào lần sau. Nhưng nó vẫn hơi kỳ lạ. Tại sao Sidekiq không thể tìm thấy câu hỏi bạn vừa lưu?
Điều kiện chạy đua giữa các quá trình
Cuộc gọi đường ray after_save
gọi lại ngay sau khi lưu bản ghi. Nhưng bản ghi đó không thể được nhìn thấy bởi các kết nối cơ sở dữ liệu khác, như kết nối cơ sở dữ liệu Sidekiq đang sử dụng, cho đến khi cơ sở dữ liệu giao dịch được cam kết, điều này sẽ xảy ra muộn hơn một chút. Điều này có nghĩa là có khả năng Sidekiq sẽ cố gắng tìm câu hỏi của bạn sau khi bạn lưu nó, nhưng trước khi bạn cam kết. Nó không thể tìm thấy bản ghi của bạn và nó phát nổ.
Vấn đề này phổ biến đến nỗi Sidekiq có một mục Câu hỏi thường gặp về nó. Và có một cách khắc phục dễ dàng.
Thay vì after_save
:
class Question < ActiveRecord::Base
after_save :index_for_search
# ...
end
sử dụng after_commit
:
class Question < ActiveRecord::Base
after_commit :index_for_search
# ...
end
Và công việc của bạn sẽ không phải xếp hàng cho đến khi Sidekiq có thể nhìn thấy mô hình của bạn.
Vì vậy, khi bạn xếp hàng đợi một công việc nền hoặc thông báo cho một quy trình khác về thay đổi bạn vừa thực hiện, hãy sử dụng after_commit
. Nếu bạn không làm vậy, họ có thể không tìm thấy bản ghi mà bạn vừa chạm vào.
Nhưng còn một vấn đề nữa…
OK, bạn đã chuyển một loạt after_save
của mình móc để sử dụng after_commit
thay vì. Mọi thứ dường như hoạt động. Đã đến lúc kiểm tra tất cả và về nhà, phải không?
Trước tiên, bạn sẽ muốn chạy thử nghiệm của mình:
require 'test_helper'
class QuestionTest < ActiveSupport::TestCase
test "A saved question is queued for indexing" do
assert_enqueued_with(job: QuestionIndexerJob) do
Question.create(title: "Is it legal to kill a zombie?")
end
end
end
1) Failure:
QuestionTest#test_A_saved_question_is_queued_for_indexing [/Users/jweiss/Source/testapps/after_commit/test/models/question_test.rb:7]:
No enqueued job found with {:job=>QuestionIndexerJob}
Rất tiếc! Không phải bài kiểm tra đã xếp hàng đợi công việc? Điều gì vừa xảy ra ở đó?
Theo mặc định, Rails kết thúc mỗi trường hợp thử nghiệm trong giao dịch cơ sở dữ liệu của riêng nó. Điều này thực sự có thể tăng tốc mọi thứ. Chỉ cần một lệnh cơ sở dữ liệu để hoàn tác tất cả các thay đổi bạn đã thực hiện trong quá trình kiểm tra.
Nhưng điều này cũng có nghĩa là after_commit
của bạn gọi lại sẽ không chạy. Vì after_commit
lệnh gọi lại chỉ chạy khi ngoài cùng giao dịch đã được cam kết.
Khi bạn gọi save
bên trong trường hợp thử nghiệm, nó vẫn cam kết một giao dịch (nhiều hơn hoặc ít hơn), nhưng đó là giao dịch ngoài cùng thứ hai giao dịch ngay bây giờ. Vì vậy, after_commit
của bạn lệnh gọi lại sẽ không chạy khi bạn mong đợi. Và bạn không thể kiểm tra điều gì xảy ra bên trong chúng.
Vấn đề này cũng có một cách khắc phục dễ dàng. Bao gồm test_after_commit
gem trong Gemfile của bạn:
group :test do
gem "test_after_commit"
end
Và after_commit
của bạn móc sẽ chạy sau giây đến cuối cùng của bạn cam kết giao dịch. Đó là những gì bạn mong đợi sẽ xảy ra.
Bạn có thể nghĩ, “Thật kỳ lạ. Tại sao tôi phải sử dụng toàn bộ một viên ngọc riêng biệt để kiểm tra một lệnh gọi lại đi kèm với Rails? Nó không nên tự động xảy ra sao? ”
Bạn đúng. Nó thật quái đảng. Nhưng nó sẽ không tồn tại lâu.
Sau khi Rails 5 xuất xưởng, bạn sẽ không phải lo lắng về test_after_commit
. Vì sự cố này đã được khắc phục trong Rails khoảng một tháng trước.
Trong mã của riêng tôi, tôi sử dụng after_commit
nhiều. Tôi có lẽ sử dụng nó nhiều hơn là sử dụng after_save
! Nhưng nó không đến mà không có các vấn đề và các trường hợp cạnh kỳ lạ.
Tuy nhiên, phiên bản này đang ngày càng tốt hơn. Và khi bạn sử dụng after_commit
ở đúng nơi, rất nhiều trường hợp ngoại lệ ngẫu nhiên, kỳ lạ sẽ không còn xảy ra nữa.