Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Làm chủ Ruby Exception Handling: Retrying, Ensuring, và Reraising

Raised exceptions có thể rescued để execute alternative code path khi things go wrong, nhưng có more ways handle exceptions. Edition AppSignal Academy này go over retry và ensure keywords, và look at reraising rescued exceptions.

Giả sử communicate với unreliable web API. Ngoài down every once in a while, nó slow đến nỗi requests có thể take seconds. Library depend on API này, cần make it as resilient as possible.

ensure

ensure keyword dùng cho ensuring block of code runs, even khi exception happens.

Trong library, muốn ensure TCP connection opened bởi Net::HTTP.start closed, even nếu request fail vì timeout, ví dụ. Làm này, wrap request trong begin/ensure/end block. Code trong ensure part luôn run, even nếu exception raised trong preceding begin block.

Trong ensure block, ensure close TCP connection bằng gọi Net::HTTP#finish trừ khi http variable là nil, có thể happen opening TCP connection fail (sẽ raise exception).

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

Note: Close TCP connection manually để allow dùng connection khi retry sau. Tuy nhiên, vì Net::HTTP.start takes block handle ensuring connection closed, sample trên rewrite remove ensure. Interestingly, ensure block cũng là cách implement trong Net::HTTP itself.

retry

retry keyword cho retry piece of code trong block. Combined với rescue block, dùng try again nếu fail open connection, hoặc API too slow respond.

Làm đó, add read_timeout cho Net::HTTP.start call set timeout 10 seconds. Nếu response request không come in by then, raise Net::ReadTimeout.

Cũng match on Errno::ECONNREFUSED handle API down completely, prevent open TCP connection. Trong case đó, http variable là nil.

Exception rescued và retry called start begin block again, result code make same request cho đến no timeout. Reuse http object hold connection nếu already exists.

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

Giờ request retry every second cho đến no Net::ReadTimeout raised.

$ 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)

Mặc dù make sure no exception raised cho any timeout ever, retry-hammering như này certainly won't help get API back up. Problematic vì code keep looping forever nếu API remains unresponsive. Thay vào đó, spread retries và give up after a while.

Giving up: reraising exceptions dùng raise

Khi exception rescued, raised exception object pass cho rescue block. Dùng extract data từ exception, như print message to log, nhưng cũng dùng reraise exact same exception, same stack trace.

begin
  raise "Exception!"
rescue RuntimeError => e
  puts "Exception happened: #{e}"
  raise e
end

Vì access exception object trong rescue block, log error to console hoặc error monitor. Thực tế, rescue và reraise chính là cách AppSignal integrations track errors.

Note: Ruby store last raised exception trong variable $!, và raise keyword dùng it by default. Gọi raise without arguments reraise last exception.

Trong library, dùng reraising take pressure off API sau couple retries. Làm đó, track number retries trong retries variable.

Khi timeout happen, increment number và check nếu <= 3, vì muốn retry 3 retries at most. Nếu yes, retry. If not, raise reraise last exception.

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

Dùng retries variable trong call sleep, increase wait time cho every new attempt.

$ 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 `
'

Request retried 3 times trước code give up và reraise last error. Handle error one level up, hoặc crash app nếu không finish job without API response.

Resilient web API client

Bằng combine these methods, built resilient web API client trong ~20 lines code. Sẽ retry requests nếu down hoặc unresponsive, give up khi không come back up again.

Hy vọng bạn học mới về handling exceptions và would love to know thoughts bạn về bài này (hoặc other ones AppSignal Academy series). Đừng ngần ngại let us know what you think, hoặc Ruby subjects bạn muốn learn more.