ข้อยกเว้นที่เพิ่มขึ้นสามารถได้รับการช่วยเหลือเพื่อรันเส้นทางรหัสทางเลือกเมื่อมีสิ่งผิดปกติ แต่มีหลายวิธีในการจัดการข้อยกเว้น ใน AppSignal Academy ฉบับนี้ เราจะพูดถึง retry
และ ensure
คีย์เวิร์ด และเราจะพิจารณาการเพิ่มข้อยกเว้นที่ได้รับการช่วยเหลือ
สมมติว่าเรากำลังสื่อสารกับ Web API ที่ไม่น่าเชื่อถือ นอกจากจะหยุดทำงานเป็นระยะ ๆ มันช้ามากจนอาจใช้เวลาไม่กี่วินาที ห้องสมุดของเราขึ้นอยู่กับ API นี้ และเราจำเป็นต้องทำให้มีความยืดหยุ่นมากที่สุด
ensure
ensure
คีย์เวิร์ดใช้สำหรับ มั่นใจ บล็อกของรหัสทำงานแม้ว่าจะมีข้อยกเว้นเกิดขึ้น
ในไลบรารีของเรา เราต้องการให้แน่ใจว่าการเชื่อมต่อ TCP เปิดโดย Net::HTTP.start
ถูกปิด แม้ว่าคำขอจะล้มเหลวเพราะหมดเวลา เป็นต้น ในการทำเช่นนี้ ก่อนอื่นเราจะรวมคำขอของเราเป็น begin
/ensure
/end
บล็อก. รหัสใน ensure
ส่วนหนึ่งจะทำงานเสมอ แม้ว่าจะมีข้อยกเว้นใน begin
. ก่อนหน้า บล็อก
ใน ensure
บล็อก เราจะปิดการเชื่อมต่อ TCP โดยเรียก Net::HTTP#finish
เว้นแต่ http
ตัวแปร nil
ซึ่งสามารถเกิดขึ้นได้การเปิดการเชื่อมต่อ TCP ล้มเหลว (ซึ่งจะทำให้เกิดข้อยกเว้น)
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
หมายเหตุ :เราปิดการเชื่อมต่อ TCP ด้วยตนเองเพื่อให้เราใช้การเชื่อมต่อเมื่อลองอีกครั้งในภายหลัง อย่างไรก็ตาม เนื่องจาก Net::HTTP.start
ใช้บล็อกที่จัดการเพื่อให้แน่ใจว่าการเชื่อมต่อถูกปิด ตัวอย่างข้างต้นสามารถเขียนใหม่เพื่อลบ ensure
. ที่น่าสนใจคือ บล็อก sure นั้นเป็นวิธีที่นำไปใช้ใน Net::HTTP ด้วยตัวมันเอง
retry
retry
คีย์เวิร์ดอนุญาตให้ลองโค้ดใหม่อีกครั้งในบล็อก ร่วมกับ rescue
บล็อก เราสามารถใช้มันเพื่อลองอีกครั้งหากเราไม่สามารถเปิดการเชื่อมต่อได้ หรือหาก API ใช้เวลาในการตอบสนองนานเกินไป
ในการทำเช่นนั้น เราจะเพิ่ม read_timeout
ไปที่ Net::HTTP.start
โทรซึ่งตั้งค่าการหมดเวลาเป็น 10 วินาที หากการตอบสนองต่อคำขอของเรายังไม่มาในตอนนั้น มันจะเพิ่ม Net::ReadTimeout
.
เรายังจะจับคู่กับ Errno::ECONNREFUSED
เพื่อจัดการกับ API ที่หยุดทำงานอย่างสมบูรณ์ ซึ่งจะทำให้เราไม่สามารถเปิดการเชื่อมต่อ TCP ได้ ในกรณีนั้น http
ตัวแปร nil
.
ข้อยกเว้นได้รับการช่วยเหลือและ retry
ถูกเรียกให้เริ่ม begin
บล็อกอีกครั้งซึ่งส่งผลให้รหัสทำการร้องขอเดียวกันจนกว่าจะไม่มีการหมดเวลา เราจะนำ http
. กลับมาใช้ใหม่ วัตถุที่มีการเชื่อมต่อหากมีอยู่แล้ว
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
ตอนนี้ คำขอของเราจะลองใหม่ทุกวินาทีจนกว่าจะไม่มี Net::ReadTimeout
ถูกยกขึ้น
$ 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)
แม้ว่าสิ่งนี้จะทำให้แน่ใจได้ว่าไม่มีข้อยกเว้นเกิดขึ้นสำหรับการหมดเวลา แต่การลองตอกย้ำแบบนี้จะไม่ช่วยให้ API นั้นกลับมาทำงานอีกครั้งอย่างแน่นอน นี่เป็นปัญหาเนื่องจากโค้ดนี้จะวนซ้ำตลอดไปหาก API ยังไม่ตอบสนอง เราควรกระจายการลองใหม่ของเราและยอมแพ้หลังจากนั้นไม่นาน
ยอมแพ้:ยกข้อยกเว้นขึ้นใหม่โดยใช้ raise
เมื่อข้อยกเว้นได้รับการช่วยเหลือ ออบเจ็กต์ข้อยกเว้นที่ยกขึ้นจะถูกส่งไปยัง rescue
บล็อก. เราสามารถใช้สิ่งนั้นเพื่อดึงข้อมูลจากข้อยกเว้น เช่น การพิมพ์ข้อความไปยังบันทึก แต่เรายังสามารถใช้เพื่อดึงข้อยกเว้นที่เหมือนกันทุกประการด้วยสแต็กเทรซเดียวกันได้
begin
raise "Exception!"
rescue RuntimeError => e
puts "Exception happened: #{e}"
raise e
end
เนื่องจากเราสามารถเข้าถึงวัตถุยกเว้นใน rescue
บล็อก เราสามารถบันทึกข้อผิดพลาดไปยังคอนโซลหรือตรวจสอบข้อผิดพลาด อันที่จริงแล้ว การช่วยเหลือและเพิ่มพูนขึ้นใหม่คือวิธีที่การรวมระบบของ AppSignal ติดตามข้อผิดพลาด
หมายเหตุ :Ruby เก็บข้อยกเว้นที่ยกขึ้นล่าสุดในตัวแปรชื่อ $!
และ raise
คีย์เวิร์ดจะใช้โดยค่าเริ่มต้น กำลังโทร raise
โดยไม่มีข้อโต้แย้งใด ๆ จะเพิ่มข้อยกเว้นสุดท้ายอีกครั้ง
ในไลบรารีของเรา เราสามารถใช้การเพิ่มใหม่เพื่อรับแรงกดดันจาก API หลังจากลองใหม่สองสามครั้ง ในการทำเช่นนั้น เราจะติดตามจำนวนครั้งของการลองใหม่ที่เราได้ทำใน retries
ตัวแปร
เมื่อใดก็ตามที่หมดเวลา เราจะเพิ่มจำนวนและตรวจสอบว่ามีค่าน้อยกว่าหรือเท่ากับสามหรือไม่ เนื่องจากเราต้องการลองใหม่อีกครั้งไม่เกินสามครั้ง ถ้าใช่ เราจะ retry
. ถ้าไม่เช่นนั้น เราจะ raise
เพื่อเพิ่มข้อยกเว้นล่าสุด
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
โดยใช้ retry
ตัวแปรในการเรียก sleep
เราสามารถเพิ่มเวลารอสำหรับความพยายามครั้งใหม่ทุกครั้งได้
$ 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 `<main>'
คำขอของเราถูกลองใหม่สามครั้งก่อนที่รหัสจะถูกยกเลิกและทำให้เกิดข้อผิดพลาดครั้งล่าสุดขึ้นใหม่ จากนั้น เราสามารถจัดการกับข้อผิดพลาดขึ้นหนึ่งระดับ หรือทำให้แอปของเราหยุดทำงานหากไม่สามารถทำงานให้เสร็จโดยปราศจากการตอบสนองของ API
ไคลเอ็นต์เว็บ API ที่ยืดหยุ่น
ด้วยการรวมวิธีการเหล่านี้ เราได้สร้างไคลเอ็นต์ API ของเว็บที่ยืดหยุ่นได้ในโค้ดประมาณ 20 บรรทัด จะลองส่งคำขออีกครั้งหากระบบหยุดทำงานหรือไม่ตอบสนอง และเราจะยกเลิกเมื่อไม่กลับมาทำงานอีก
เราหวังว่าคุณจะได้เรียนรู้สิ่งใหม่ๆ เกี่ยวกับการจัดการข้อยกเว้น และอยากทราบว่าคุณคิดอย่างไรกับบทความนี้ (หรือบทความอื่นๆ ในซีรี่ส์ AppSignal Academy) โปรดอย่าลังเลที่จะแจ้งให้เราทราบว่าคุณคิดอย่างไร หรือหากคุณมีวิชา Ruby ที่ต้องการเรียนรู้เพิ่มเติม