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

การช่วยเหลือข้อยกเว้นใน Ruby:A Primer

ที่ 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