Tại AppSignal, chúng tôi cung cấp tính năng theo dõi lỗi cho các ứng dụng Ruby. Để làm như vậy, chúng tôi nắm bắt tất cả các ứng dụng ngoại lệ ném vào chúng tôi và thông báo cho các nhà phát triển khi chúng xảy ra.
Có thể khó để có được quyền xử lý ngoại lệ. Trong bài viết này, chúng tôi sẽ giải thích cách hoạt động của nó, những vấn đề nào mà việc xử lý không tốt có thể gây ra và cách giải cứu các ngoại lệ đúng cách.
Giải quyết các trường hợp ngoại lệ
Bằng cách cứu các ngoại lệ trong Ruby, bạn có thể ngăn ứng dụng của mình gặp sự cố ngay khi có sự cố. Với begin .. rescue
chặn bạn có thể chỉ định một đường dẫn thay thế cho ứng dụng của mình khi xảy ra lỗi.
begin
File.read "config.yml"
rescue
puts "No config file found. Using defaults."
end
Cũng có thể chỉ định những trường hợp ngoại lệ nào nên được giải cứu. Khi chỉ định một lớp ngoại lệ, tất cả các lớp con của ngoại lệ này cũng sẽ được ghi lại.
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
Trong ví dụ trên, bạn có thể thấy ngoại lệ Errno::ENOENT
bị bắt khi cha mẹ của nó SystemCallError
đang được giải cứu.
Giải cứu quá cao trong chuỗi ngoại lệ
Điều quan trọng là không giải cứu các ngoại lệ quá cao trong chuỗi Ngoại lệ. Khi bạn làm như vậy, tất cả các ngoại lệ của lớp con cũng sẽ bị bắt, làm cho việc nắm bắt của khối cứu hộ quá chung chung.
Đây là chương trình đọc tệp cấu hình dựa trên đối số được truyền vào chương trình.
# $ 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
Thông báo lỗi cho biết nó không thể đọc tệp cấu hình, nhưng vấn đề thực sự là lỗi đánh máy trong mã.
begin
File.read config_file
rescue => e
puts e.inspect
end
#<NoMethodError: undefined method `firs' for []:Array>
Lớp ngoại lệ mặc định được bắt bởi begin .. rescue
khối là StandardError. Nếu chúng ta không vượt qua một lớp cụ thể, Ruby sẽ giải cứu StandardError và tất cả các lỗi thuộc lớp con. NoMethodError là một trong những lỗi này.
Việc cứu một lớp ngoại lệ cụ thể sẽ giúp ngăn các lỗi không liên quan vô tình gây ra trạng thái lỗi. Nó cũng cho phép các thông báo lỗi tùy chỉnh cụ thể hơn hữu ích hơn cho người dùng cuối.
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
Giải cứu ngoại lệ
Nó có thể vẫn còn hấp dẫn để giải cứu lên cao trong chuỗi ngoại lệ. Việc khắc phục tất cả các lỗi mà một ứng dụng có thể mắc phải sẽ giúp nó không bị treo. (Chúng tôi đến đây 100% thời gian hoạt động!) Tuy nhiên, nó có thể gây ra nhiều vấn đề.
Lớp Exception là lớp ngoại lệ chính trong Ruby. Tất cả các ngoại lệ khác là các lớp con của lớp này; nếu Exception được giải cứu, tất cả các lỗi sẽ bị bắt.
Hai ngoại lệ mà hầu hết các ứng dụng sẽ không muốn giải cứu là SignalException và SystemExit.
SignalException được sử dụng khi một nguồn bên ngoài yêu cầu ứng dụng dừng lại. Đây có thể là Hệ điều hành khi nó muốn tắt hoặc quản trị viên hệ thống muốn dừng ứng dụng. Ví dụ
SystemExit được sử dụng khi
exit
đang được gọi từ ứng dụng Ruby. Khi điều này được nâng lên, nhà phát triển muốn ứng dụng dừng lại. Ví dụ
Nếu chúng tôi cứu Exception và những ngoại lệ này được đưa ra trong khi ứng dụng hiện đang chạy
begin ... rescue ... end
chặn nó không thể thoát.
Nói chung, đó là một ý tưởng tồi để giải cứu Exception trong các tình huống bình thường. Khi cứu Exception, bạn sẽ ngăn SignalException và SystemExit hoạt động, nhưng cũng có thể kể đến LoadError, SyntaxError và NoMemoryError. Thay vào đó, tốt hơn nên giải cứu các ngoại lệ cụ thể hơn.
Lỗi trong các bài kiểm tra
Khi Exception được giải cứu, sử dụng rescue Exception => e
, những thứ khác bên cạnh ứng dụng của bạn có thể bị hỏng. Bộ thử nghiệm thực sự có thể đang ẩn một số lỗi.
Trong các xác nhận nhỏ nhất và RSpec không thành công sẽ đưa ra một ngoại lệ để thông báo cho bạn về xác nhận không thành công, không thực hiện được bài kiểm tra. Khi làm như vậy, họ nâng cao các ngoại lệ tùy chỉnh của riêng mình, được phân loại từ Exception.
Nếu Exception được giải cứu trong một thử nghiệm hoặc trong mã ứng dụng, nó có thể đang làm câm một lỗi xác nhận.
# 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
Mong đợi các trường hợp ngoại lệ
Một số mã có nghĩa là để nâng cao các ngoại lệ. Trong một bộ thử nghiệm, bạn có thể chỉ cần tắt tiếng ngoại lệ để kiểm tra không bị lỗi khi chúng được nâng lên.
def foo
raise RuntimeError, "something went wrong"
end
foo rescue RuntimeError
Tuy nhiên, điều này không kiểm tra xem có một ngoại lệ nào được đưa ra hay không. Khi ngoại lệ không được nêu ra, thử nghiệm của bạn sẽ không thể biết được hành vi có còn đúng hay không.
Có thể khẳng định nếu ngoại lệ được nêu ra, và nếu không, ngoại lệ nào là.
# 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)>'
Ngoại lệ tăng lại
Một ứng dụng chỉ nên nắm bắt các ngoại lệ cao trong chuỗi như lớp Ngoại lệ khi có một lý do chính đáng. Ví dụ:khi có một số thao tác dọn dẹp liên quan trước khi thoát khỏi một khối mã, chẳng hạn như xóa các tệp tạm thời thực sự cần phải xóa.
Một khuyến nghị khi bạn hoàn toàn phải cứu Exception, hãy nâng lại nó sau khi bạn xử lý xong lỗi. Bằng cách này, việc xử lý ngoại lệ Ruby có thể quyết định số phận của quá trình sau này.
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
Bạn không chắc nên giải cứu điều gì?
Như đã đề cập trước đó, tốt nhất là bạn nên xác định cụ thể những lỗi nào cần khắc phục.
Khi bạn không chắc chắn về những ngoại lệ nào mà một hoạt động có thể gây ra, việc giải cứu StandardError có thể là một nơi tốt để bắt đầu. Chạy mã của bạn trong các tình huống khác nhau và xem nó tạo ra những ngoại lệ nào.
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>
Mỗi khi bạn gặp một ngoại lệ mới, hãy thêm các trường hợp giải cứu cụ thể cho các ngoại lệ đó hoặc lớp cha có liên quan của nó. Tốt hơn là nên giải cứu cụ thể những gì cần giải cứu hơn là giải cứu quá nhiều trường hợp ngoại lệ.
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
Điều này kết thúc phần sơ lược của chúng tôi về việc xử lý các ngoại lệ trong Ruby. Hãy cho chúng tôi biết tại @AppSignal nếu bạn muốn biết thêm hoặc có câu hỏi cụ thể. Nếu bạn muốn hiểu rõ hơn về vị trí và tần suất các ngoại lệ được đưa ra trong ứng dụng của mình, hãy dùng thử AppSignal.