Preface

File.open("/etc/hosts", "r") do |f| content << f.read end

Threat from return

def with_file(name, &block) puts "Open file" f = File.open(name, "r") yield f puts "Close file" f.close end

def test_yield content = "" with_file("/etc/hosts") do |f| puts "Read content" content << f.read end content end

Open file Read content Close file

def test_yield_with_return content = "" with_file("/etc/hosts") do |f| puts "Read content" content << f.read return content end puts "I will be skipped" end

Open file Read content

1. Enter test_yield # def test_yield 2. Execute code till with_file # content = "" 3. .... Enter with_file # with_file("/etc/hosts") do |f| .... # .... # def with_file(name, &block) 4. .... Execute code till yield # puts "Open file" .... # f = File.open(name, "r") 5. ........ Enter block via yield # yield f 6. ........ Execute code in block # puts "Read content" ........ # content << f.read 7. ........ Leave block # 8. .... Execute code after yield # puts "Close file" .... # f.close 9. .... Leave with_file # 10. Execute code after with_file # content 11. Leave test_yield #

1. Enter test_yield_with_return # def test_yield_with_return 2. Execute code till with_file # content = "" 3. .... Enter with_file # with_file("/etc/hosts") do |f| .... # .... # def with_file(name, &block) 4. .... Execute code till yield # puts "Open file" .... # f = File.open(name, "r") 5. ........ Enter block via yield # yield f 6. ........ Execute till return # puts "Read content" ........ # content << f.read 7. ........ Leave block via return # return content 8. .... Leave with_file # 9. Leave test_yield_with_return #

def ensured_with_file(name, &block) puts "Open file" f = File.open(name, "r") yield f ensure puts "Close file" f.close end def test_yield_and_return_again ensured_with_file("/etc/hosts") do |f| puts "Read content" return f.read end end

Open file Read content Close file

What’s about exception?

def test_yield_with_exception with_file("/etc/hosts") do |f| puts "Read content" 1 / 0 # oops end end

Open file Read content ZeroDivisionError: divided by 0

def test_yield_with_exception_handling ensured_with_file("/etc/hosts") do |f| puts "Read content" 1 / 0 # oops end end

Open file Read content Close file ZeroDivisionError: divided by 0

Ensuring everything

User.create do |u| u.firstname = "Chuck" u.lastname = "Norris" u.balance = 1 / 0 #oops u.email = "gmail@chucknorris.com" end

Ensure in the wild

result = yield [cols, stmt] stmt.result_metadata.free if cols stmt.free_result stmt.close if binds.empty? result

temp_file = Tempfile.new(basename(file_name), temp_dir) temp_file.binmode yield temp_file temp_file.close

Summary

How does your code behave in case of return?

How does your code behave in case of exception?

Usingand blocks is what makes Ruby so different from other scripting languages. But in some cases yield can lead to unpredictable behavior and it’s crucial to understand what can go wrong.Let’s consider next code:File is opened, used in block and automatically closed after leaving it. What can be wrong with it?Let’s createfunction which mimicsbehavior:Andto use it:produces next output:Nothing special. Now, more complicated test:Runand output isn’t so predictable:Quite weird. Why post-yield action isn’t triggered? The answer is in quirk behavior of-statement in blocks. Return from block immediately unwinds stack and exits from surrounding method. In our case it’s.To make it more clear, let’s discuss how everything works in both cases. Consider what happens in case without return(running):Now how it works with return in block (running):Now it’s clear, how return affects the whole pipeline. Afterit immediately unwinds execution stack and returns control to the point, whereis called skipping desired post-yield actions.Such code can easily lead to resource leakage, when file isn’t closed, or database connection isn’t got back to connection-pool.Let’s putafter yield to make it work properly:Now everything looks fine:You can say this example is quite contrived because return in block is used rarely. It can be true, but same behavior can be obtained when something in block raises exception:Result:File isn’t closed again, andfix this issue as well:output:But why Matz didn’t make ensuring strategy default for? Unfortunately such behavior can be an issue as well. Considermethod.Quite usual code:Ifwere put intomethod, it would create user Chuck record without balance and email fields. So, for such cases yield-by-default aren’t suitable.It’s interested whether yield is properly handled in real-world Ruby-code.We took offand applied it on Rails-related gems and Ruby Stdlib. The result was surprisingly good. All resource-sensitive code is decorated withand properly handles resource freeing.We found only two places with non-critical issues. The first is in activerecord/lib/active_record/connection_adapters/mysql_adapter.rb , method exec_stmt . At the bottom it has next snippet:Looks like stmt.* piece should be put under ensure protection.Another one is in activesupport/lib/active_support/core_ext/file/atomic.rb , method self.atomic_write This part is not critical, because GC closes all Tempfile objects properly. But for predictable behavior it’s better to wrap this part in.We glanced across other popular gems like redis, unicorn, resque and others, but didn’t find anything suspicious. Anyway, you can check by yourself most production critical gems and validate their safety.When using yield, decide which behavior is preferable for you: with or without ensure.Using third-party gems with block-based API, check whether gem author properly handle resource cleaning.Looks like Ruby Stdlib, Rails and most popular gems handle yield properly.