{"id":3085,"date":"2012-11-20T20:01:34","date_gmt":"2012-11-20T17:01:34","guid":{"rendered":"http:\/\/railsware.com\/blog\/?p=3085"},"modified":"2020-10-27T11:15:42","modified_gmt":"2020-10-27T08:15:42","slug":"yield-gotcha-in-ruby-blocks","status":"publish","type":"post","link":"https:\/\/railsware.com\/blog\/yield-gotcha-in-ruby-blocks\/","title":{"rendered":"yield-gotcha every Ruby developer should be aware of"},"content":{"rendered":"<h2>Preface<\/h2>\n\n<p>Using <strong>yield<\/strong> and blocks is what makes Ruby so different from other scripting languages. But in some cases yield can lead to unpredictable behavior and it&#8217;s crucial to understand what can go wrong.<\/p>\n\n<p>Let&#8217;s consider next code:<\/p>\n\n<pre lang=\"ruby\">File.open(\"\/etc\/hosts\", \"r\") do |f|\n  content &lt;&lt; f.read\nend\n<\/pre>\n\n<p>File is opened, used in block and automatically closed after leaving it. What can be wrong with it?<\/p>\n\n<h2>Threat from return<\/h2>\n\n<p>Let&#8217;s create <strong>with_file<\/strong> function which mimics <strong>File.open<\/strong> behavior:<\/p>\n\n<pre lang=\"ruby\">def with_file(name, &amp;block)\n  puts \"Open file\"\n  f = File.open(name, \"r\")\n  yield f\n  puts \"Close file\"\n  f.close\nend\n<\/pre>\n\n<p>And <strong>test_yield<\/strong> to use it:<\/p>\n\n<pre lang=\"ruby\">def test_yield\n  content = \"\"\n  with_file(\"\/etc\/hosts\") do |f|\n    puts \"Read content\"\n    content &lt;&lt; f.read\n  end\n  content\nend\n<\/pre>\n\n<p><strong>test_yield<\/strong> produces next output:<\/p>\n\n<pre>Open file\nRead content\nClose file\n<\/pre>\n\n<p>Nothing special. Now, more complicated test:<\/p>\n\n<pre lang=\"ruby\">def test_yield_with_return\n  content = \"\"\n  with_file(\"\/etc\/hosts\") do |f|\n    puts \"Read content\"\n    content &lt;&lt; f.read\n    return content\n  end\n  puts \"I will be skipped\"\nend\n<\/pre>\n\n<p>Run <strong>test_yield_with_return<\/strong> and output isn&#8217;t so predictable:<\/p>\n\n<pre>Open file\nRead content\n<\/pre>\n\n<p>Quite weird. Why post-yield action isn&#8217;t triggered? The answer is in quirk behavior of <strong>return<\/strong>-statement in blocks. Return from block immediately unwinds stack and exits from surrounding method.\nIn our case it&#8217;s <strong>test_yield_with_return<\/strong>.<\/p>\n\n<p>To make it more clear, let&#8217;s discuss how everything works in both cases. Consider what happens in case without return(running <strong>test_yield<\/strong>):<\/p>\n\n<pre lang=\"ruby\"> 1. Enter test_yield                # def test_yield\n\n 2. Execute code till with_file     #   content = \"\"\n\n 3. .... Enter with_file            #   with_file(\"\/etc\/hosts\") do |f|\n    ....                            #\n    ....                            # def with_file(name, &amp;block)\n\n 4. .... Execute code till yield    #   puts \"Open file\"\n    ....                            #   f = File.open(name, \"r\")\n\n 5. ........ Enter block via yield  #   yield f\n\n 6. ........ Execute code in block  #     puts \"Read content\"\n    ........                        #     content &lt;&lt; f.read\n\n 7. ........ Leave block            #\n\n 8. .... Execute code after yield   #   puts \"Close file\"\n    ....                            #   f.close\n\n 9. .... Leave with_file            #\n \n10. Execute code after with_file    #   content\n\n11. Leave test_yield                # \n<\/pre>\n\n<p>Now how it works with return in block (running <strong>test_yield_with_return<\/strong>):<\/p>\n\n<pre lang=\"ruby\">1. Enter test_yield_with_return    # def test_yield_with_return\n\n2. Execute code till with_file     #   content = \"\"\n\n3. .... Enter with_file            #   with_file(\"\/etc\/hosts\") do |f|\n   ....                            #\n   ....                            # def with_file(name, &amp;block)\n\n4. .... Execute code till yield    #   puts \"Open file\"\n   ....                            #   f = File.open(name, \"r\")\n\n5. ........ Enter block via yield  #   yield f\n\n6. ........ Execute till return    #     puts \"Read content\"\n   ........                        #     content &lt;&lt; f.read\n\n7. ........ Leave block via return #     return content\n\n8. .... Leave with_file            #\n \n9. Leave test_yield_with_return    # \n<\/pre>\n\n<p>Now it&#8217;s clear, how return affects the whole pipeline. After <strong>step 6<\/strong> it immediately unwinds execution stack and returns control to the point, where <strong>test_yield_with_return<\/strong> is called skipping desired post-yield actions.<\/p>\n\n<p>Such code can easily lead to resource leakage, when file isn&#8217;t closed, or database connection isn&#8217;t got back to connection-pool.<\/p>\n\n<p>Let&#8217;s put <strong>ensure<\/strong> after yield to make it work properly:<\/p>\n\n<pre lang=\"ruby\">def ensured_with_file(name, &amp;block)\n  puts \"Open file\"\n  f = File.open(name, \"r\")\n  yield f\nensure\n  puts \"Close file\"\n  f.close\nend\n \ndef test_yield_and_return_again\n  ensured_with_file(\"\/etc\/hosts\") do |f|\n    puts \"Read content\"\n    return f.read\n  end\nend\n<\/pre>\n\n<p>Now everything looks fine:<\/p>\n\n<pre>Open file\nRead content\n<\/pre>\n\n<p><strong>Close file<\/strong><\/p>\n\n<pre><\/pre>\n\n<h2>What&#8217;s about exception?<\/h2>\n\n<p>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:<\/p>\n\n<pre lang=\"ruby\">def test_yield_with_exception\n  with_file(\"\/etc\/hosts\") do |f|\n    puts \"Read content\"\n    1 \/ 0 # oops\n  end\nend\n<\/pre>\n\n<p>Result:<\/p>\n\n<pre>Open file\nRead content\nZeroDivisionError: divided by 0\n<\/pre>\n\n<p>File isn&#8217;t closed again, and <strong>ensure<\/strong> fix this issue as well:<\/p>\n\n<pre lang=\"ruby\">def test_yield_with_exception_handling\n  ensured_with_file(\"\/etc\/hosts\") do |f|\n    puts \"Read content\"\n    1 \/ 0 # oops\n  end\nend\n<\/pre>\n\n<p><strong>test_yield_with_exception_handling<\/strong> output:<\/p>\n\n<pre>Open file\nRead content\n<\/pre>\n\n<p><strong>Close file<\/strong><\/p>\n\n<pre>ZeroDivisionError: divided by 0\n<\/pre>\n\n<h2>Ensuring everything<\/h2>\n\n<p>But why Matz didn&#8217;t make ensuring strategy default for <strong>yield<\/strong>? Unfortunately such behavior can be an issue as well. Consider <strong>ActiveRecord::Base.create<\/strong> method.<\/p>\n\n<p>Quite usual code:<\/p>\n\n<pre lang=\"ruby\">User.create do |u|\n  u.firstname = \"Chuck\"\n  u.lastname = \"Norris\"\n  u.balance = 1 \/ 0 #oops\n  u.email = \"gmail@chucknorris.com\"\nend\n<\/pre>\n\n<p>If <strong>ensure<\/strong> were put into <strong>create<\/strong> method, it would create user Chuck record without <em>balance<\/em> and <em>email<\/em> fields. So, for such cases yield-by-default aren&#8217;t suitable.<\/p>\n\n<h2>Ensure in the wild<\/h2>\n\n<p>It&#8217;s interested whether yield is properly handled in real-world Ruby-code.<\/p>\n\n<p>We took off <strong>grep<\/strong> and applied it on Rails-related gems and Ruby Stdlib. The result was surprisingly good. All resource-sensitive code is decorated with <strong>ensure<\/strong> and properly handles resource freeing.<\/p>\n\n<p>We found only two places with non-critical issues. The first is in\n<a href=\"https:\/\/github.com\/rails\/rails\/blob\/3-2-stable\/activerecord\/lib\/active_record\/connection_adapters\/mysql_adapter.rb\">activerecord\/lib\/active_record\/connection_adapters\/mysql_adapter.rb<\/a>, method <a href=\"https:\/\/github.com\/rails\/rails\/blob\/3-2-stable\/activerecord\/lib\/active_record\/connection_adapters\/mysql_adapter.rb#L355\">exec_stmt<\/a>.\nAt the bottom it has next snippet:<\/p>\n\n<pre lang=\"ruby\">result = yield [cols, stmt]\n\nstmt.result_metadata.free if cols\nstmt.free_result\nstmt.close if binds.empty?\n\nresult\n<\/pre>\n\n<p>Looks like stmt.* piece should be put under ensure protection.<\/p>\n\n<p>Another one is in <a href=\"https:\/\/github.com\/rails\/rails\/blob\/3-2-stable\/activesupport\/lib\/active_support\/core_ext\/file\/atomic.rb\">activesupport\/lib\/active_support\/core_ext\/file\/atomic.rb<\/a>, method <a href=\"https:\/\/github.com\/rails\/rails\/blob\/3-2-stable\/activesupport\/lib\/active_support\/core_ext\/file\/atomic.rb#L15\">self.atomic_write<\/a>:<\/p>\n\n<pre lang=\"ruby\">temp_file = Tempfile.new(basename(file_name), temp_dir)\ntemp_file.binmode\nyield temp_file\ntemp_file.close\n<\/pre>\n\n<p>This part is not critical, because GC closes all Tempfile objects properly. But for predictable behavior it&#8217;s better to wrap this part in <strong>ensure<\/strong>.<\/p>\n\n<p>We glanced across other popular gems like redis, unicorn, resque and others, but didn&#8217;t find anything suspicious. Anyway, you can check by yourself most production critical gems and validate their safety.<\/p>\n\n<h2>Summary<\/h2>\n\n<p><strong>1.<\/strong> When using yield, decide which behavior is preferable for you: with or without ensure.<\/p>\n\n<ul>\n    <li>How does your code behave in case of return?<\/li>\n    <li>How does your code behave in case of exception?<\/li>\n<\/ul>\n\n<p><strong>2.<\/strong> Using third-party gems with block-based API, check whether gem author properly handle resource cleaning.<\/p>\n\n<p><strong>3.<\/strong> Looks like Ruby Stdlib, Rails and most popular gems handle yield properly.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Preface 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&#8217;s crucial to understand what can go wrong. Let&#8217;s consider next code: File.open(&#8220;\/etc\/hosts&#8221;, &#8220;r&#8221;) do |f| content &lt;&lt; f.read end File is opened, used in block and automatically closed&#8230;<\/p>\n","protected":false},"author":25,"featured_media":9472,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[3],"tags":[],"coauthors":["Sergii Boiko"],"class_list":["post-3085","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development"],"acf":[],"aioseo_notices":[],"categories_data":[{"name":"Engineering","link":"https:\/\/railsware.com\/blog?category=development"}],"post_thumbnails":"https:\/\/railsware.com\/blog\/wp-content\/themes\/railsware\/vendors\/images\/article-thumbnail-default.jpg","amp_enabled":true,"_links":{"self":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/3085","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/users\/25"}],"replies":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/comments?post=3085"}],"version-history":[{"count":70,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/3085\/revisions"}],"predecessor-version":[{"id":13941,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/3085\/revisions\/13941"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/media\/9472"}],"wp:attachment":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/media?parent=3085"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/categories?post=3085"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/tags?post=3085"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/coauthors?post=3085"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}