yield gotcha

yield-gotcha every Ruby developer should be aware of

| 9 Comments

Preface

Yield sign

Using yield and 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?

Threat from return

Let’s create with_file function which mimics File.open behavior:

And test_yield to use it:

test_yield produces next output:

Nothing special. Now, more complicated test:

Run test_yield_with_return and output isn’t so predictable:

Quite weird. Why post-yield action isn’t triggered? The answer is in quirk behavior of return-statement in blocks. Return from block immediately unwinds stack and exits from surrounding method. In our case it’s test_yield_with_return.

To make it more clear, let’s discuss how everything works in both cases. Consider what happens in case without return(running test_yield):

Now how it works with return in block (running test_yield_with_return):

Now it’s clear, how return affects the whole pipeline. After step 6 it immediately unwinds execution stack and returns control to the point, where test_yield_with_return is 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 put ensure after yield to make it work properly:

Now everything looks fine:

What’s about exception?

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, and ensure fix this issue as well:

test_yield_with_exception_handling output:

Ensuring everything

But why Matz didn’t make ensuring strategy default for yield? Unfortunately such behavior can be an issue as well. Consider ActiveRecord::Base.create method.

Quite usual code:

If ensure were put into create method, it would create user Chuck record without balance and email fields. So, for such cases yield-by-default aren’t suitable.

Ensure in the wild

It’s interested whether yield is properly handled in real-world Ruby-code.

We took off grep and applied it on Rails-related gems and Ruby Stdlib. The result was surprisingly good. All resource-sensitive code is decorated with ensure and 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 ensure.

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.

Summary

1. When using yield, decide which behavior is preferable for you: with or without ensure.

  • How does your code behave in case of return?
  • How does your code behave in case of exception?

2. Using third-party gems with block-based API, check whether gem author properly handle resource cleaning.

3. Looks like Ruby Stdlib, Rails and most popular gems handle yield properly.

Share
* Railsware is a premium software development consulting company, focused on delivering great web and mobile applications. Learn more about us.

Want to get more of Railsware blog?

RSS FEED

We're always ready to help!

CONTACT US