16.09.2009Обработка исключений в руби
Введение
Сегодняшняя запись — это первая запись по заявкам читателей. :) Я напоминаю, что если у вас включен JavaScript, и если вы читаете эту запись на сайте, а не в rss-читалке, то вдоль левой границы окна будет оранжевая кнопка. Можно написать своё предложение либо нажав на неё, либо непосредственно на странице проекта. За предложения можно так же голосовать.
Итак, сегодня речь пойдёт об обработке исключений в руби. Если вы хотите, чтобы программа была надёжной. Чтобы когда все процессы упали, а оборудование отказало, программа спокойно констатировала это, продолжая работать. В таком случае все ошибки и исключения должны быть обработаны.
Базовые блоки
Я не буду слишком подробно останавливаться на каждом составляющем, а просто приведу пример, сделав некоторые пояснения:
class RetryException < StandardError
attr :can_retry
def initialize(rtr = true)
@can_retry = rtr
end
end
def exception_handling
begin # Если бы не было else, то возвращалось бы последнее значение begin
yield if block_given?
rescue ZeroDivisionError # Если програму запускает не Чак Норрис
"На ноль делить нельзя"
rescue RetryException => re # Возврат перезапускает блок begin
puts "Не получилось, но мы попробуем снова"
if re.can_retry
retry
else
"Теперь точно не получилось"
end
rescue # Здесь ловится всё остальное
puts "Случилось непредвиденное: #{$!.inspect}"
raise
else # Если всё прошло без ошибок
"Всё прошло без ошибок"
ensure # Этот блок выполняется в любом случае
puts "Процесс окончен, но эта часть ничего не возвращает"
"В любом случае" # Этой строки мы нигде не увидим
end
end
blocks = [] # Массив с блоками
blocks.push(lambda {})
blocks.push(lambda { 1/0 })
blocks.push(lambda do
@retry ||= 3 # Пробуем ещё раз не более трёх раз
@retry -= 1
raise RetryException.new(@retry > 0), "Временная ошибка"
end)
blocks.push(lambda { raise "Неведомая ошибка" })
blocks.each do |block|
puts "Возвратилось: #{exception_handling(&block) rescue "Ошибка!"}"
puts "------------------------"
end
Результат выполнения программы выглядит следующим образом:
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Всё прошло без ошибок
------------------------
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: На ноль делить нельзя
------------------------
Не получилось, но мы попробуем снова
Не получилось, но мы попробуем снова
Не получилось, но мы попробуем снова
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Теперь точно не получилось
------------------------
Случилось непредвиденное: #<RuntimeError: Неведомая ошибка>
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Ошибка!
------------------------
Что мы отсюда почерпнули:
- Блок else (необязательный) выполняется в случае удачно выполненного begin и возвращает значение вместо него.
- Выполняется первый из блоков rescue, который соответствует ошибке
- Метод raise без параметров выбрасывает текущее исключение: то, которое находится в глобальной переменной $!
- Чтобы создать свой класс исключений, наследовать нужно от StandardError или от его потомков, иначе не сработает rescue без указания класса (попробуйте сами проверить)
Обратите внимание, что когда мы знаем, что вызов может бросить в нас ошибкой, то ловим её ещё раз. Запись
a = some_call rescue "some value"
эквивалентна
a = begin
some_call
rescue
"some value"
end
Особенности работы с потоками
Когда дело касается Thread лучше всего обрабатывать ошибки внутри него. Но если это невозможно, то есть пара вещей, которые необходимо знать.
Во-первых, без специальных указаний, никто даже не узнает о том, что в каком-то из потоков произошла ошибка:
begin
Thread.new do
sleep 1
puts "Первый поток"
end
Thread.new do
sleep 3
puts "Последний поток"
end
Thread.new do
sleep 2
raise "Ошибка внутри потока"
end
rescue
puts "Спасены!"
end
puts "Поехали!"
sleep 7
puts "Как-то слишком тихо"
Производит вывод:
Поехали!
Первый поток
Последний поток
Как-то слишком тихо
Первый способ засечь исключения -- это вызвать join на потоке. Тогда исключение будет переброшено в сам join. Но этот метод блокирует программу, пока поток не завершится. Что конечно же не очень удобно. Наше «поехали» перемещается ближе к концу:
begin
Thread.new do
sleep 1
puts "Первый поток"
end
Thread.new do
sleep 3
puts "Последний поток"
end
Thread.new do
sleep 2
raise "Ошибка внутри потока"
end.join
rescue Exception => e
puts "Спасены!"
puts e.backtrace.join("\n")
end
puts "Поехали!"
sleep 7
Производит вывод:
Первый поток
Спасены!
thr.rb:16
thr.rb:14:in `join'
thr.rb:14
Поехали!
Последний поток
Что занимательно, никакие другие потоки не умерли, когда в одном из них произошла ошибка. Если мы хотим, чтобы все потоки умерли, когда происходит ошибка, которая не обработана внутри потока, то нужно написать в самом начале:
Thread.abort_on_exception = true
begin
Thread.new do
sleep 1
puts "Первый поток"
end
Thread.new do
sleep 3
puts "Последний поток"
end
Thread.new do
sleep 2
raise "Ошибка внутри потока"
end
rescue Exception => e
puts "Спасены!"
end
puts "Поехали!"
sleep 7
Тогда наша программа оборвётся в момент ошибки и выведет:
Поехали!
Первый поток
thr.rb:16: Ошибка внутри потока (RuntimeError)
from thr.rb:14:in `initialize'
from thr.rb:14:in `new'
from thr.rb:14
И никакие rescue нас не спасут.