ไม่ใช่ทุก 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 - ครั้งแรก
continuationvariable assigned คือ return value ของcallccblock - ทุกครั้งกระโดดกลับ savepoint
continuationvariable assigned argument ที่ส่งให้callmethod ดังนั้น 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 ไม่เคยเกิด