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

Master Ruby Exception Handling: Retrying, Ensuring, และ Reraising

Raised exceptions สามารถ rescued ได้เพื่อ execute alternative code path เมื่อ things go wrong แต่มี more ways เพื่อ handle exceptions ใน edition ของ AppSignal Academy นี้ เราจะไป over retry และ ensure keywords และดู reraising rescued exceptions

สมมติเรา communicate กับ unreliable web API นอกจากมัน down ทุกครั้งในเมื่อ มันช้ามากที่ requests ไปมันใช้ seconds Library ของเรา depend บน API นี้ และเราต้องทำให้มัน resilient ที่สุด

ensure

Keyword ensure ใช้สำหรับ ensuring block ของ code runs แม้เมื่อ exception happens

ใน library ของเรา เราอยาก ensure TCP connection ที่เปิดโดย Net::HTTP.start ถูก close แม้ request จะ fail เพราะ timeout ตัวอย่าง เพื่อทำนี้ เราจะ wrap request ใน begin/ensure/end block Code ใน ensure จะ run เสมอ แม้ exception จะ raised ใน begin block ก่อนหน้า

ใน ensure block เราจะ ensure ปิด TCP connection โดยเรียก Net::HTTP#finish เว้นแต่ http variable จะเป็น nil ซึ่งอาจเกิดขึ้นถ้า opening TCP connection fail (ซึ่งก็ raise exception)

require "net/http" begin puts "Opening TCP connection..." http = Net::HTTP.start(uri.host, uri.port) puts "Sending HTTP request..." puts http.request_get(uri.path).body ensure if http puts "Closing the TCP connection..." http.finish end end

Note: เราปิด TCP connection manually เพื่อให้เราสามารถใช้ connection เมื่อ retry ภายหลัง อย่างไรก็ตาม เนื่องจาก Net::HTTP.start รับ block ซึ่ง handle ensuring connection ถูกปิด sample ด้านบนสามารถ rewrite ให้ remove ensure ได้ สนุกคือ ensure block นี้ก็เป็นวิธีที่ implement ใน Net::HTTP เอง

retry

Keyword retry อนุญาต retry code ใน block ร่วมกับ rescue block เราสามารถใช้มัน try again ถ้าเรา fail เปิด connection หรือ API ใช้เวลานานเกินไปตอบสนอง

เพื่อทำนั้น เราจะเพิ่ม read_timeout ให้ Net::HTTP.start call ซึ่งตั้ง timeout เป็น 10 วินาที ถ้า response ไม่มาในเวลานั้น จะ raise Net::ReadTimeout

เราจะ match บน Errno::ECONNREFUSED เพื่อ handle API down completely ซึ่งจะ prevent เราเปิด TCP connection ในกรณีนั้น http variable จะเป็น nil

Exception ถูก rescued และ retry ถูกเรียกเพื่อเริ่ม begin block ใหม่ ซึ่งผลลัพธ์คือ code ทำ same request จนกว่าไม่มี timeout เกิดขึ้น เราจะ reuse http object ที่เก็บ connection ถ้ามันมีอยู่แล้ว

require "net/http" http = nil uri = URI("https://localhost:4567/") begin unless http puts "Opening TCP connection..." http = Net::HTTP.start(uri.host, uri.port, read_timeout: 10) end puts "Executing HTTP request..." puts http.request_get(uri.path).body rescue Errno::ECONNREFUSED, Net::ReadTimeout => e puts "Timeout (#{e}), retrying in 1 second..." sleep(1) retry ensure if http puts "Closing the TCP connection..." http.finish end end

ตอนนี้ request ของเราจะ retry ทุกวินาทีจนกว่าไม่มี Net::ReadTimeout raised

$ ruby retry.rb Opening TCP connection... Executing HTTP request... Timeout (Net::ReadTimeout), retrying in 1 second... Executing HTTP request... Timeout (Net::ReadTimeout), retrying in 1 second... Executing HTTP request... Timeout (Net::ReadTimeout), retrying in 1 second... Executing HTTP request... ... (in an endless loop) 

แม้จะ make sure ไม่มี exception raised สำหรับ timeout ไหนๆ retry-hammering เหมือนนี้แน่นอนจะไม่ช่วยให้ API กลับมาขึ้นอีก นี่ problematic เพราะ code นี้จะ loop forever ถ้า API ยังคง unresponsive แทนที่จะเราควร spread retries และ give up หลังจากพักให้นานพอ

Giving up: reraising exceptions ใช้ raise

เมื่อ exception ถูก rescued raised exception object ถูกส่งให้ rescue block เราสามารถใช้มัน extract data จาก exception เช่น print message ไป log แต่เรายังสามารถใช้มัน reraise exact same exception พร้อม same stack trace

begin raise "Exception!" rescue RuntimeError => e puts "Exception happened: #{e}" raise e end

เพราะเรามี access ไป exception object ใน rescue block เราสามารถ log error ไป console หรือ error monitor จริงๆ rescuing และ reraising คือวิธีที่ AppSignal's integrations track errors

Note: Ruby เก็บ last raised exception ใน variable ชื่อ $! และ keyword raise จะใช้มันโดยค่าเริ่มต้น การเรียก raise โดยไม่มี arguments จะ reraise last exception

ใน library ของเรา เราสามารถใช้ reraising เพื่อ take pressure off API หลังจาก several retries เพื่อทำนี้ เราจะ track number ของ retries ที่เราทำใน variable retries

ทุกครั้งที่ timeout เกิดขึ้น เราจะ increment number และ check ถ้ามัน less than หรือ equal to three เพราะเราอยาก retry สูงสุดสามครั้ง ถ้าใช่ เราจะ retry ถ้าไม่ เราจะ raise เพื่อ reraise last exception

require "net/http" http = nil uri = URI("https://localhost:4567/") retries = 0 begin unless http puts "Opening TCP connection..." http = Net::HTTP.start(uri.host, uri.port, read_timeout: 1) end puts "Executing HTTP request..." puts http.request_get(uri.path).body rescue Errno::ECONNREFUSED, Net::ReadTimeout => e if (retries += 1) <= 3 puts "Timeout (#{e}), retrying in #{retries} second(s)..." sleep(retries) retry else raise end ensure if http puts 'Closing the TCP connection...' http.finish end end

ด้วยการใช้ variable retries ในการเรียก sleep เราสามารถเพิ่ม wait time สำหรับทุก attempt ใหม่

$ ruby reraise.rb Opening TCP connection... Executing HTTP request... Timeout (Net::ReadTimeout), retrying in 1 second(s)... Executing HTTP request... Timeout (Net::ReadTimeout), retrying in 2 second(s)... Executing HTTP request... Timeout (Net::ReadTimeout), retrying in 3 second(s)... Executing HTTP request... Closing the TCP connection... /lib/ruby/2.4.0/net/protocol.rb:176:in `rbuf_fill': Net::ReadTimeout (Net::ReadTimeout) ... from reraise.rb:13:in `
'

Request ของเราถูก retry สามครั้งก่อนที่ code จะ give up และ reraise last error เราสามารถ handle error one level up หรือ crash app ถ้ามันไม่สามารถ finish job โดยไม่มี API's response

A resilient web API client

ด้วยการ combine methods เหล่านี้ เราสร้าง resilient web API client ในประมาณ twenty lines ของ code มันจะ retry requests ถ้ามัน down หรือ unresponsive และเราจะ give up เมื่อมันไม่กลับมา up อีก

หวังว่าคุณเรียนรู้อะไรใหม่เกี่ยวกับ handling exceptions และเรายินดีรู้ความคิดเห็นของคุณเกี่ยวกับบทความนี้ (หรือ articles อื่นๆ ใน AppSignal Academy series) โปรดอย่าลังเลบอกเราว่าคุณคิดยังไง หรือถ้ามี Ruby subjects ที่อยากเรียนรู้เพิ่มเติม