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

เชี่ยวชาญ Ruby Exception Handling: วิธี Retry Operations เมื่อเกิด Errors

ไม่ใช่ทุก errors ที่ fatal บางอย่างแค่บอกว่า "ลองใหม่" โชคดีที่ Ruby มี mechanism สำหรับ "ลองใหม่" - แม้ไม่ได้ชัดเจนหรือรู้จักกันดี ในโพสต์นี้ดู mechanisms เหล่านี้และการทำงานในโลกจริง

แนะนำ retry

อันนี้ค่อนข้างชัดเจน ถ้ารู้ว่ามีอยู่ ผมอยู่ใน Ruby career นานก่อนรู้จัก keyword "retry" ที่น่ารักนี้

Retry built-in ใน exception rescuing system ของ Ruby เรียบง่าย: ใช้ "retry" ใน rescue block จะทำให้ code ระหว่าง begin-rescue รันใหม่ ดูตัวอย่าง:


begin
  retries ||= 0
  puts "try ##{ retries }"
  raise "the roof"
rescue
  retry if (retries += 1) < 3
end
# ... outputs the following:
# try #0
# try #1
# try #2

สังเกตจุดสำคัญ:

  • เวลา retry เรียก ทั้งหมด code ระหว่าง begin-rescue รันใหม่ ไม่ได้** "ต่อจากที่หยุด" หรือแบบนั้น
  • ถ้าไม่ limit retries จะ infinite loop
  • Code ใน begin และ rescue blocks เข้าถึง retries variable เดียวกันใน parent scope ได้

ปัญหากับ retry

Retry ดีแต่มี limitations หลักคือทั้ง begin block รันใหม่ บางครั้งไม่ ideal

เช่น ใช้ gem โพสต์ status ไป Twitter, Facebook, หลาย sites ด้วย single method call อาจเป็นแบบนี้:


SocialMedia.post_to_all("Zomg! I just ate the biggest hamburger")
# ...posts to Twitter API
# ...posts to Facebook API
# ...etc

ถ้า API หนึ่ง fail ตอบสนอง gem raise SocialMedia::TimeoutError และ abort ถ้าจับ exception และ retry จะได้ duplicate posts เพราะ retry เริ่มต้นใหม่


begin
  SocialMedia.post_to_all("Zomg! I just ate the biggest hamburger")
rescue SocialMedia::TimeoutError
  retry
end
# ...posts to Twitter API
# facebook error
# ...posts to Twitter API
# facebook error
# ...และไปเรื่อยๆ

จะดีมั้ยถ้าบอก gem "แค่ข้าม facebook ไป ทำต่อ APIs ต่อไป" โชคดีที่ Ruby ทำได้!

หมายเหตุ: แน่นอน real solution คือ re-architect social media library แต่ไม่อาจเป็น use-case เพียงอย่างเดียวสำหรับเทคนิคที่จะแสดง

Continuations ช่วยชีวิต

Continuations ทำให้คนตกใจ แต่เพราะใช้บ่อยและดูแปลก แต่เข้าใจพื้นฐานแล้วง่ายมาก

Continuation เหมือน "save point" ในโค้ด เหมือนในเกมวิดีโอ ไปทำอย่างอื่น แล้วกระโดดกลับ save point ทุกอย่างเหมือนเดิม

...ไมใช่ analogy สมบูรณ์ แต่ใช้ได้ โค้ด:


require "continuation"
counter = 0
continuation = callcc { |c| c } # define savepoint
puts(counter += 1)
continuation.call(continuation) if counter < 5 # jump back to savepoint

อาจสังเกตสิ่งแปลกๆ มาดู:

  • ใช้ callcc สร้าง Continuation object ไม่มี clean OO syntax
  • ครั้งแรก continuation variable assigned คือ return value ของ callcc block
  • ทุกครั้งกระโดดกลับ savepoint continuation variable assigned argument ที่ส่งให้ call method ดังนั้น syntax แปลก continuation.call(continuation)

เพิ่ม Continuations ให้ Exceptions

เราจะใช้ continuations เพิ่มเมธอด skip ให้ทุก exceptions ตัวอย่างด้านล่างแสดงการทำงาน: เวลา rescue exception เรียก skip code ที่ raise exception จะทำงานเหมือนไม่เคยเกิด


begin
  raise "the roof"
  puts "The exception was ignored"
rescue => e
  e.skip
end
# ...outputs "The exception was ignored"

ทำได้โดย commit บาปนิดหน่อย Exception เป็นคลาส แปลกว่า monkey-patch เพิ่ม skip method:


class Exception
  attr_accessor :continuation
  def skip
    continuation.call
  end
end

ตอนนี้ตั้งค่า continuation attribute ให้ทุก exception raise เป็นแค่เมธอด override ได้

โค้ดด้านล่างมาจาก slide deck ของ Avdi "Things You Didn't know about Exceptions" ไม่มีวิธี implement ดีกว่า:


require 'continuation'
module StoreContinuationOnRaise
  def raise(*args)
    callcc do |continuation|
      begin
        super
      rescue Exception => e
        e.continuation = continuation
        super(e)
      end
    end
  end
end

class Object
  include StoreContinuationOnRaise
end

ตอนนี้เรียก skip method สำหรับ exception ใดๆ ได้ มันจะเหมือน exception ไม่เคยเกิด