ที่ AppSignal เรามีการติดตามข้อผิดพลาดสำหรับแอปพลิเคชัน Ruby ในการดำเนินการดังกล่าว เราจะรวบรวมแอปพลิเคชันข้อยกเว้นทั้งหมดที่ส่งมาที่เราและแจ้งให้นักพัฒนาทราบทันที
อาจเป็นเรื่องยากที่จะได้รับการจัดการข้อยกเว้นที่ถูกต้อง ในบทความนี้ เราจะอธิบายวิธีการทำงาน ปัญหาที่อาจเกิดจากการจัดการที่ไม่ดี และวิธีแก้ไขข้อยกเว้นอย่างเหมาะสม
ข้อยกเว้นการช่วยเหลือ
การช่วยเหลือข้อยกเว้นใน Ruby คุณสามารถป้องกันไม่ให้แอปพลิเคชันของคุณหยุดทำงานในขณะที่มีบางอย่างผิดพลาด ด้วย begin .. rescue บล็อก คุณสามารถระบุเส้นทางอื่นสำหรับแอปพลิเคชันของคุณเมื่อเกิดข้อผิดพลาด
begin
File.read "config.yml"
rescue
puts "No config file found. Using defaults."
end นอกจากนี้ยังสามารถระบุข้อยกเว้นที่ควรได้รับการช่วยเหลือ เมื่อระบุคลาสข้อยกเว้น คลาสย่อยทั้งหมดของข้อยกเว้นนี้จะถูกดักจับด้วย
begin
File.read "config.yml"
rescue SystemCallError => e
puts e.class # => Errno::ENOENT
puts e.class.superclass # => SystemCallError
puts e.class.superclass.superclass # => StandardError
end
ในตัวอย่างข้างต้น คุณจะเห็นข้อยกเว้น Errno::ENOENT ถูกจับเมื่อ SystemCallError parent กำลังได้รับการช่วยเหลือ
การช่วยชีวิตที่สูงเกินไปในห่วงโซ่ข้อยกเว้น
สิ่งสำคัญคือต้องไม่ช่วยเหลือข้อยกเว้นให้สูงเกินไปในห่วงโซ่ข้อยกเว้น เมื่อคุณทำเช่นนั้น ข้อยกเว้นของคลาสย่อยทั้งหมดจะถูกตรวจจับด้วย ทำให้การดักจับของบล็อกกู้ภัยนั้นกว้างเกินไป
นี่คือโปรแกรมที่อ่านไฟล์ปรับแต่งตามอาร์กิวเมนต์ที่ส่งไปยังโปรแกรม
# $ ruby example.rb config.yml
def config_file
ARGV.firs # Note the typo here, we meant `ARGV.first`.
end
begin
File.read config_file
rescue
puts "Couldn't read the config file"
end ข้อความแสดงข้อผิดพลาดแจ้งว่าไม่สามารถอ่านไฟล์ปรับแต่งได้ แต่ปัญหาที่แท้จริงคือการสะกดผิดในโค้ด
begin
File.read config_file
rescue => e
puts e.inspect
end
#<NoMethodError: undefined method `firs' for []:Array>
คลาสข้อยกเว้นเริ่มต้นถูกจับโดย begin .. rescue บล็อกคือ StandardError หากเราไม่ผ่านในคลาสใดคลาสหนึ่ง Ruby จะช่วยเหลือ StandardError และข้อผิดพลาดของคลาสย่อยทั้งหมด NoMethodError เป็นหนึ่งในข้อผิดพลาดเหล่านี้
การช่วยเหลือคลาสข้อยกเว้นเฉพาะจะช่วยป้องกันข้อผิดพลาดที่ไม่เกี่ยวข้องจากการแจ้งสถานะความล้มเหลวโดยไม่ตั้งใจ นอกจากนี้ยังช่วยให้มีข้อความแสดงข้อผิดพลาดที่กำหนดเองซึ่งเป็นประโยชน์มากขึ้นสำหรับผู้ใช้ปลายทาง
config_file = "config.yml"
begin
File.read config_file
rescue Errno::ENOENT => e
puts "File or directory #{config_file} doesn't exist."
rescue Errno::EACCES => e
puts "Can't read from #{config_file}. No permission."
end ข้อยกเว้นการช่วยเหลือ
มันอาจจะยังน่าดึงดูดใจที่จะช่วยเหลือให้สูงขึ้นในห่วงโซ่ข้อยกเว้น การแก้ไขข้อผิดพลาดทั้งหมดที่แอปพลิเคชันสามารถทำให้เกิดการหยุดทำงาน (มาถึงแล้ว 100% uptime!) อย่างไรก็ตาม อาจทำให้เกิดปัญหาได้มากมาย
คลาส Exception เป็นคลาสข้อยกเว้นหลักใน Ruby ข้อยกเว้นอื่นๆ ทั้งหมดเป็นคลาสย่อยของคลาสนี้ หากได้รับการยกเว้น ข้อผิดพลาดทั้งหมดจะถูกจับ
ข้อยกเว้นสองประการที่แอปพลิเคชันส่วนใหญ่ไม่ต้องการช่วยเหลือคือ SignalException และ SystemExit
SignalException ใช้เมื่อแหล่งภายนอกกำลังบอกให้แอปพลิเคชันหยุดทำงาน นี่อาจเป็นระบบปฏิบัติการเมื่อต้องการปิดระบบ หรือผู้ดูแลระบบที่ต้องการหยุดแอปพลิเคชัน ตัวอย่าง
SystemExit ใช้เมื่อ
exitกำลังถูกเรียกจากแอปพลิเคชัน Ruby เมื่อสิ่งนี้ถูกยกขึ้น ผู้พัฒนาต้องการให้แอปพลิเคชันหยุดทำงาน ตัวอย่าง
หากเราช่วยเหลือ Exception และข้อยกเว้นเหล่านี้เกิดขึ้นในขณะที่แอปพลิเคชันกำลังทำงาน
begin ... rescue ... endบล็อกมันออกไม่ได้
เป็นความคิดที่ไม่ดีในการช่วยเหลือ Exception ในสถานการณ์ปกติ เมื่อช่วยชีวิต Exception คุณจะป้องกันไม่ให้ SignalException และ SystemExit ทำงาน แต่ยังรวมถึง LoadError, SyntaxError และ NoMemoryError เป็นต้น ดีกว่าที่จะบันทึกข้อยกเว้นที่เฉพาะเจาะจงมากขึ้นแทน
การทดสอบล้มเหลว
เมื่อ Exception ได้รับการช่วยเหลือ โดยใช้ rescue Exception => e สิ่งอื่นนอกเหนือจากแอปพลิเคชันของคุณอาจเสียหายได้ ชุดทดสอบอาจซ่อนข้อผิดพลาดอยู่บ้าง
ในการยืนยันแบบ minitest และ RSpec ที่ล้มเหลวจะทำให้เกิดข้อยกเว้นเพื่อแจ้งให้คุณทราบเกี่ยวกับการยืนยันที่ล้มเหลว การทดสอบไม่ผ่าน เมื่อทำเช่นนั้น พวกเขาจะยกข้อยกเว้นที่กำหนดเองของตนเอง โดยจัดคลาสย่อยจากข้อยกเว้น
หากได้รับการช่วยเหลือในการทดสอบหรือในโค้ดของแอปพลิเคชัน ข้อยกเว้นอาจเป็นการปิดเสียงยืนยันความล้มเหลว
# RSpec example
def foo(bar)
bar.baz
rescue Exception => e
puts "This test should actually fail"
# Failure/Error: bar.baz
# <Double (anonymous)> received unexpected message :baz with (no args)
end
describe "#foo" do
it "hides an 'unexpected message' exception" do
bar = double(to_s: "")
foo(bar)
end
end คาดว่าจะมีข้อยกเว้น
รหัสบางรหัสมีขึ้นเพื่อเพิ่มข้อยกเว้น ในชุดทดสอบ เป็นไปได้ที่จะปิดเสียงข้อยกเว้นเพื่อให้การทดสอบไม่ล้มเหลวเมื่อถูกยกขึ้น
def foo
raise RuntimeError, "something went wrong"
end
foo rescue RuntimeError อย่างไรก็ตาม สิ่งนี้ไม่ได้ทดสอบว่ามีการยกข้อยกเว้นหรือไม่ เมื่อไม่มีการยกข้อยกเว้น การทดสอบของคุณก็จะไม่สามารถบอกได้ว่าพฤติกรรมนั้นยังถูกต้องอยู่หรือไม่
เป็นไปได้ที่จะยืนยันหากมีการยกข้อยกเว้นขึ้น และถ้าไม่ มีข้อยกเว้นใด
# expecting_exceptions_spec.rb
# RSpec example
def foo
raise NotImplementedError, "foo method not implemented"
end
describe "#foo" do
it "raises a RuntimeError" do
expect { foo }.to raise_error(RuntimeError)
end
end 1) #foo raises a RuntimeError
Failure/Error: expect { foo }.to raise_error(RuntimeError)
expected RuntimeError, got #<NotImplementedError: foo method not implemented> with backtrace:
# ./expecting_exceptions_spec.rb:4:in `foo'
# ./expecting_exceptions_spec.rb:9:in `block (3 levels) in <top (required)>'
# ./expecting_exceptions_spec.rb:9:in `block (2 levels) in <top (required)>'
# ./expecting_exceptions_spec.rb:9:in `block (2 levels) in <top (required)>'
เพิ่มข้อยกเว้นอีกครั้ง
แอปพลิเคชันควรดักจับข้อยกเว้นที่สูงกว่าในสายโซ่เป็นคลาส Exception เมื่อมีเหตุผลที่ดีมากเท่านั้น ตัวอย่างเช่น เมื่อมีการล้างข้อมูลก่อนที่จะออกจากกลุ่มโค้ด เช่น การลบไฟล์ชั่วคราวที่จำเป็นต้องลบจริงๆ
คำแนะนำข้อหนึ่งสำหรับเวลาที่คุณจำเป็นต้องกู้คืน Exception อย่างแท้จริง ให้เพิ่มใหม่อีกครั้งหลังจากที่คุณจัดการข้อผิดพลาดเสร็จแล้ว วิธีนี้การจัดการข้อยกเว้นของ Ruby สามารถตัดสินชะตากรรมของกระบวนการได้ในภายหลัง
File.open("/tmp/my_app.status", "w") { |f| "running" }
begin
foo
rescue Exception => e
Appsignal.add_error e
File.open("/tmp/my_app.status", "w") { |f| "stopped" }
raise e
end ไม่แน่ใจว่าจะช่วยเหลืออะไรดี?
ดังที่ได้กล่าวไว้ก่อนหน้านี้ เป็นการดีที่จะระบุเฉพาะข้อผิดพลาดที่จะช่วยเหลือ
เมื่อคุณไม่แน่ใจว่ามีข้อยกเว้นใดบ้างในการดำเนินการ การช่วยเหลือ StandardError อาจเป็นจุดเริ่มต้นที่ดี รันโค้ดของคุณในสถานการณ์ต่างๆ และดูว่ามีข้อยกเว้นอะไรบ้าง
begin
File.open('/tmp/appsignal.log', 'a') { |f| f.write "Starting AppSignal" }
rescue => e
puts e.inspect
end
#<Errno::EACCES: Permission denied @ rb_sysopen - /tmp/appsignal.log> ทุกครั้งที่คุณพบข้อยกเว้นใหม่ ให้เพิ่มกรณีช่วยเหลือเฉพาะสำหรับข้อยกเว้นเหล่านั้นหรือคลาสพาเรนต์ที่เกี่ยวข้อง ระบุเฉพาะว่าจะช่วยเหลืออะไรดีกว่าช่วยเหลือข้อยกเว้นมากเกินไป
begin
file = '/tmp/appsignal.log'
File.open(file, 'a') { |f| f.write("AppSignal started!") }
rescue Errno::ENOENT => e
puts "File or directory #{file} doesn't exist."
rescue Errno::EACCES => e
puts "Cannot write to #{file}. No permissions."
end
# Or, using the parent error class
begin
file = '/tmp/appsignal.log'
File.open(file, 'a')
rescue SystemCallError => e
puts "Error while writing to file #{file}."
puts e
end นี่เป็นการสรุปไพรเมอร์ของเราเกี่ยวกับการจัดการข้อยกเว้นใน Ruby แจ้งให้เราทราบที่ @AppSignal หากคุณต้องการทราบข้อมูลเพิ่มเติมหรือมีคำถามเฉพาะ หากคุณต้องการได้รับข้อมูลเชิงลึกที่ดีขึ้นเกี่ยวกับตำแหน่งและความถี่ในการยกข้อยกเว้นในแอปของคุณ ให้ลองใช้ AppSignal