Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

Callback Pitfalls ใน Rails 5 และวิธีแก้

ActiveRecord callbacks เรียก code ระหว่าง stages ต่างๆ ของ model life ง่าย

เช่น Q&A site อยาก search ทุก questions ทุกครั้งเปลี่ยน question จะ index ใน ElasticSearch Indexing ใช้เวลา ไม่วน ย้าย background ด้วย Sidekiq

ดูเหมาะสำหรับ after_save callback! ใน model:

app/models/question.rb
class Question < ActiveRecord::Base after_save :index_for_search # ... private def index_for_search QuestionIndexerJob.perform_later(self) end end
app/jobs/question_indexer_job.rb
class QuestionIndexerJob < ActiveJob::Base queue_as :default def perform(question) # ... index the question ... end end

ใช้งานได้ดี! หรือดูเหมือน จน queue jobs เยอะ และเห็น errors:

2015-03-10T05:29:02.881Z 52530 TID-oupf889w4 WARN: Error while trying to deserialize arguments: Couldn't find Question with 'id'=3

Sidekiq จะ retry และน่าจะทำงานครั้งหน้า แตแปลกหน่อย ทำไม Sidekiq หา question ที่เพิ่ง save ไม่เจอ?

Race Condition ระหว่าง Processes

Rails เรียก after_save callbacks ทันทีหลัง record save แต record นั้นไม่ถูกมองเห็นโดย database connections อื่น (เช่น Sidekiq) จนกว่า database transaction จะ committed ซึ่งเกิดขึ้นภายหลังนิดหน่อย หมายถึงมีโอกาส Sidekiq หา question หลัง save แตก่อน commit หา record ไม่เจอ และพัง

ปัญหานี้ common มาก Sidekiq มี FAQ entry แตแก้ง่าย

แทน after_save:

app/models/question.rb
class Question < ActiveRecord::Base after_save :index_for_search # ... end

用 after_commit:

app/models/question.rb
class Question < ActiveRecord::Base after_commit :index_for_search # ... end

Job จะไม่ถูก queue จนกว่า Sidekiq จะเห็น model

ตอน queue background job หรือบอก process อื่นเกี่ยวกับการเปลี่ยนแปลง 用 after_commit ไม่งั้นอาจหา record ที่เพิ่งสัมผัสไม่เจอ

แตยังมีปัญหาอีก…

โอเค เปลี่ยน after_save hooks เป็น after_commit ทุกอย่างดู work ไป check in กลับบ้านไหม?

ก่อน รัน tests:

test/models/question_test.rb
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}

Whoops! Test ควร queue job ไหม? เกิดอะไรขึ้น?

Default Rails wrap แต่ละ test case ใน database transaction ของตัวเอง เร่งความเร็ว มาแค่ 1 database command undo การเปลี่ยนแปลงใน test

แตมันหมายถึง after_commit callback ไม่รัน เพราะ after_commit callbacks รันเฉพาะตอน outermost transaction committed

ตอน save ใน test case ยัง commit transaction (มากหรือน้อย) แตเป็น second-most-outermost transaction ตอนนี้ after_commit callbacks ไม่รันตอนคาดหวัง และทดสอบสิ่งใน callbacks ไม่ได้

ปัญหานี้แกง่าย 加 test_after_commit gem ใน Gemfile:

Gemfile
group :test do gem "test_after_commit" end

after_commit hooks จะรันหลัง second-to-last transaction commit ซึ่งเป็นสิ่งที่คาดหวัง

อาจคิดว่า "แปลก ต้อง用 gem แยก test callback ที่มากับ Rails? ควร automatic ไหม?"

ถูกต้อง แปลก แตไม่คอยนาน

พอ Rails 5 shipped ไม่ต้องกังวล test_after_commit เพราะปัญหานี้ fixed ใน Rails ประมาณเดือนก่อน

ในโค้ดผม 用 after_commit เยอะ มากกว่า after_save! แตไม่ได้ไม่มีปัญหาและ edge cases แปลก

Version โดย version กำลังดีขึ้น และ用 after_commit ในที่ถูกต้อง weird, random exceptions เยอะจะไม่เกิดแล้ว