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

Gotchas โทรกลับสองสามอัน (และ Rails 5 Fix)

การเรียกกลับของ ActiveRecord เป็นวิธีที่ง่ายในการเรียกใช้โค้ดในช่วงต่างๆ ของชีวิตโมเดลของคุณ

ตัวอย่างเช่น สมมติว่าคุณมีไซต์ถาม &ตอบ และคุณต้องการค้นหาคำถามทั้งหมดได้ ทุกครั้งที่คุณเปลี่ยนแปลงคำถาม คุณจะต้องจัดทำดัชนีในบางอย่าง เช่น ElasticSearch การจัดทำดัชนีใช้เวลาสักครู่และไม่เร่งด่วน ดังนั้นคุณจะทำในเบื้องหลังด้วย Sidekiq

ดูเหมือนว่าจะเป็นเวลาที่เหมาะสมที่สุดในการใช้ after_save โทรกลับ! ดังนั้นในแบบจำลองของคุณ คุณจะเขียนบางอย่างเช่น:

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

มันใช้งานได้ดี! หรืออย่างน้อยก็ดูเหมือนว่าจะ จนกว่าคุณจะรอคิวงานมากขึ้นและเห็นข้อผิดพลาดเหล่านี้ปรากฏขึ้น:

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

แน่นอนว่า Sidekiq จะลองงานใหม่อีกครั้ง และมันอาจจะใช้ได้ในครั้งต่อไป แต่ก็ยังแปลกอยู่นิดหน่อย เหตุใด Sidekiq จึงไม่พบคำถามที่คุณเพิ่งบันทึกไว้

สภาวะการแข่งขันระหว่างกระบวนการ

Rails โทร after_save โทรกลับทันทีหลังจากบันทึก แต่การเชื่อมต่อฐานข้อมูลอื่นไม่สามารถเห็นบันทึกนั้นได้ เช่นเดียวกับที่ Sidekiq กำลังใช้ จนกว่าฐานข้อมูลจะ ธุรกรรม เป็นความมุ่งมั่นซึ่งจะเกิดขึ้นภายหลังเล็กน้อย ซึ่งหมายความว่ามีโอกาสที่ Sidekiq จะพยายามค้นหาคำถามของคุณหลังจากที่คุณบันทึกแล้ว แต่ก่อนที่คุณจะส่งคำถาม ไม่พบบันทึกของคุณ และมันก็ระเบิด

ปัญหานี้เป็นเรื่องธรรมดามากที่ Sidekiq มีรายการคำถามที่พบบ่อยเกี่ยวกับเรื่องนี้ และมีวิธีแก้ไขที่ง่าย

แทน 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

และงานของคุณจะไม่ถูกเข้าคิวจนกว่า Sidekiq จะมองเห็นโมเดลของคุณ

ดังนั้น เมื่อคุณจัดคิวงานพื้นหลังหรือบอกกระบวนการอื่นเกี่ยวกับการเปลี่ยนแปลงที่คุณเพิ่งทำ ให้ใช้ after_commit . หากไม่เป็นเช่นนั้น พวกเขาอาจไม่พบบันทึกที่คุณเพิ่งสัมผัส

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

โอเค คุณเปลี่ยน after_save . ของคุณไปหลายชุดแล้ว ขอใช้ after_commit แทนที่. ดูเหมือนว่าทุกอย่างจะได้ผล ได้เวลาเช็คของแล้วกลับบ้านได้ใช่มั้ย

ก่อนอื่น คุณจะต้องทำการทดสอบ:

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}

อ๊ะ! การทดสอบไม่ควรเข้าคิวงาน? เกิดอะไรขึ้นที่นั่น

ตามค่าเริ่มต้น Rails จะล้อมแต่ละกรณีทดสอบในธุรกรรมฐานข้อมูลของตนเอง สิ่งนี้สามารถเร่งความเร็วได้จริงๆ ใช้คำสั่งฐานข้อมูลเพียงคำสั่งเดียวเพื่อยกเลิกการเปลี่ยนแปลงทั้งหมดที่คุณทำระหว่างการทดสอบ

แต่นี่ก็หมายถึง after_commit . ของคุณ การโทรกลับจะไม่ทำงาน เพราะว่า after_commit การโทรกลับจะทำงานเฉพาะเมื่อ อยู่นอกสุด ได้ตกลงทำธุรกรรมแล้ว

เมื่อคุณโทร save ภายในกรณีทดสอบ มันยังคงทำธุรกรรม (มากหรือน้อย) แต่นั่นคือ รองจากนอกสุด การทำธุรกรรมในขณะนี้ ดังนั้น after_commit . ของคุณ การโทรกลับจะไม่ทำงานเมื่อคุณคาดหวัง และคุณไม่สามารถทดสอบสิ่งที่เกิดขึ้นภายในพวกเขาได้

ปัญหานี้ยังมีการแก้ไขที่ง่าย รวม test_after_commit อัญมณีใน Gemfile ของคุณ:

Gemfile
group :test do
  gem "test_after_commit"
end

และ after_commit . ของคุณ hooks จะทำงานต่อจาก ตัวสุดท้าย . ของคุณ การทำธุรกรรม ซึ่งเป็นสิ่งที่คุณคาดหวังให้เกิดขึ้น

คุณอาจจะคิดว่า “นั่นแปลก เหตุใดฉันจึงต้องใช้ gem แยกต่างหากทั้งหมดเพื่อทดสอบการโทรกลับที่มาพร้อมกับ Rails มันควรจะเกิดขึ้นโดยอัตโนมัติไม่ใช่หรือ”

คุณถูก. มันแปลก. แต่มันจะไม่แปลกอีกต่อไป

เมื่อ Rails 5 จัดส่งแล้ว คุณจะไม่ต้องกังวลกับ test_after_commit . เนื่องจากปัญหานี้ได้รับการแก้ไขแล้วใน Rails เมื่อประมาณหนึ่งเดือนที่แล้ว

ในรหัสของฉันเอง ฉันใช้ after_commit มาก. ฉันอาจจะใช้มันมากกว่าที่ฉันใช้ after_save ! แต่มันก็ไม่ได้มาโดยไม่มีปัญหาและเคสที่ล้ำสมัย

เวอร์ชันต่อเวอร์ชัน ดีขึ้นเรื่อยๆ และเมื่อคุณใช้ after_commit ในสถานที่ที่เหมาะสม ข้อยกเว้นแบบสุ่มๆ แปลกๆ มากมายจะไม่เกิดขึ้นอีกต่อไป