The Trifecta of FAIL; or, how to patch Rails 2.0 for Ruby 1.8.7

It’s an oft-stated fact that most disasters result not from a single point of failure but from a combination of failures reinforcing each other. I wouldn’t term the problem I ran into last Friday a disaster, but it certainly cost me several hours of time trying to find a workaround.

Culprit #1: Rails

Rails’ ActiveSupport added a handy little method called #chars to the String class. In and of itself this doesn’t seem like such a bad thing, and a lot of other handy methods in ActiveSupport are built on top of #chars. However, as we’ll see, taking advantage of Ruby’s open classes to extend core types has a way of drawing the unwanted attention from the Law of Unintended Consequences.

Culprit #2: Ruby

It’s not set in stone anywhere, but there’s a fairly well accepted convention in open source projects that versions are divided into a major version, a minor version, and a tiny or patch version. New major versions indicate API-breaking changes. A new minor version may introduce new features, but existing code should continue to work as-is. And a new tiny version indicates that the API remains fixed; the only difference is that bugs have been fixed and security holes patched.

Ruby 1.8.7 is a minor release masquerading as a tiny release. Among the features backported into 1.8.7 from Ruby 1.9 is a new #chars attribute. Unfortunately, it is incompatible with the Rails 2.0 implementation of #chars. This, incidentally, is a prime example of one of the subtler ways that patching the core classes can bite you. Even if you are adding new methods rather than re-writing existing ones, the chances are good that someone else will have the same idea only with a slightly different implementation and semantics. Bang, incompatibility.

Culprit #3: MacPorts

We have an app which has not yet been ported to Rails 2.1. This, in itself, would not have been a problem; we can keep running it under Ruby 1.8.6 with Rails 2.0, no problem. However, I have a nasty habit of trying to keep my software up to date. So I run sudo port upgrade outdated periodically, and watch all the errors from unmaintained ports go scrolling across my terminal for 24 hours or so.

The last time I did this, one of the ports that did manage to build was Ruby. Version 1.8.7. The next time I ran our app, it of course promptly crashed.

This is the point at which I discovered something I hadn’t realized about MacPorts: it has no downgrade path. Coming from the world of Debian, Ubuntu, and apt-get, I just expected any package management system to handle the case where the user specifies an older version to be installed.

In fact, there’s a way to do it in MacPorts, but it’s painful.

Fail.

fail owned pwned pictures
So there I was with a broken app, no time in the iteration to upgrade it to Rails 2.1, and no easy way to get back to Ruby 1.8.6. Lame.

Rescue

After bitching and moaning on Twitter for awhile, I decided Bob helps those who help themselves, so I took a look at the crash backtrace I was getting. I traced it back to a line in vendor/rails/activesupport/lib/activesupport/core_ext/string/access.rb:

        def first(limit = 1)
          chars[0..(limit - 1)].to_s
        end

In the Rails 2.0 version of String#chars, #chars returns an Array or Array-like object which can be subscripted with #[]. The Ruby 1.8.7 version, by contrast, returns an Enumerable::Enumerator.

“That’s easy enough” thought I, and, fully expecting that patching this one issue would just reveal another incompatibility, and another, and another…, I changed the code to:

        def first(limit = 1)
          chars.to_a[0..(limit - 1)].to_s
        end

Lo and behold, the app worked perfectly.

Of course, YMMV. But as a quick kludge this one was surprisingly painless.

Lessons Learned

Here’s what I took away from this experience:

  1. Be wary of adding methods to core classes. What could possibly go wrong? More than you think.
  2. Patch releases should be true patch releases. It’s tempting to include a neat new feature as a bonus –
    again, what could possibly go wrong? Resist this urge.
  3. Macs are shiny, but for industrial-strength development support, nothing beats a Debian-based system with APT.
  4. Every now and then taking a clawhammer to vendor code is the shortest (short-term) way from point A to point B. Personally I prefer to either keep this kind of change local or, if necessary, version it with something like Piston, rather than maintaining it as a monkey-patch.
[ad#PostInline]

18 comments

  1. There are more issues at play here. First of all, the Ruby core developers have this habit of only stating their intentions in Japanese on their own mailing list. Nobody ever warned the Rails core team about the fact that 1.8.7 will eat up Chars. When Manfred and I discovered the breakage it was already too late. Never have the Ru y developers considered the implications of them reclaiminv the chars method for the core String. If you followed the recent Ruby security fiasco with official update killing everything from Mongrel to god knows what…

  2. There are more issues at play here. First of all, the Ruby core developers have this habit of only stating their intentions in Japanese on their own mailing list. Nobody ever warned the Rails core team about the fact that 1.8.7 will eat up Chars. When Manfred and I discovered the breakage it was already too late. Never have the Ru y developers considered the implications of them reclaiminv the chars method for the core String. If you followed the recent Ruby security fiasco with official update killing everything from Mongrel to god knows what…

  3. Not to start a flame war or anything, but is it really the responsibility of the Ruby core developers have to know all of the ways Rails extending core classes so they know when it’s necessary to warn people about their upcoming changes? On the other hand, the Ruby core team has a wealth of unit tests from popular projects like Rails and RubySpec to run tests with to see if their pending changes are going to break anything.

    I’m sort of in the middle on the issue. Ruby core devs could do alot better job testing against popular ruby projects, while Rails core devs could do alot better job of not extending core classes when other options are available.

  4. I can assure you that when we created the String#chars method this has been publicly praised as the good choice. We’ve chosen a very specific method for a plethora of functionality that needd to be integrated in a safe way, and to blend it into the language we’ve used one method.

    The decision to do it like this was weighted and discussed and approved by quite some people as the most appropriate for the case.

    If Ruby Core would care at all about ActiveSupport (which is now the widest deployed gem in the world I think) they would think twice before introducing it in the version of the language without Unicode support.

  5. I see a few problems here:<br><br>1. The Ruby development team included an API change (a new method) to a patch release. This should not have been done. <br><br>2. "The nasty habit" of updating and staying on the cutting edge also contributed to this problem. This isn't just a problem with Avidi, but I have also been bitten by that bad habit.<br><br>3. The Ruby language does not have a well defined spec and test case suit, which leads into problems like these.<br><br>4. Blaming the Rails development team for using a language feature (albeit an often discouraged one) is wrong.<br><br>I offer the following to try to remedy the situation:<br><br>1. Path releases should be just that. No new features should be introduced in a z (as in x.y.z) release.<br><br>2. If one does not need what a new version of any software package provides, there is no need to upgrade. However, it's an assumed fact in the open source software world that a patch release is, more often than not, safe to upgrade to, so the problem is twofold. (See 1)<br><br>3. There is <a href="http://rubyspec.org">rubyspec.org</a&gt; and all of the large Ruby based frameworks and libraries have reliable and fairly complete test suites that can be run against a Ruby release. Obviously the Ruby development team failed to test an unusual release with all the available test suites. I don't have to tell anyone how important testing is, specially when you have pretty a lot of customers.<br><br>4. Thought it is a discouraged practice, for better or worse, the ability to extend the core library (or any already defined class) is a built-in language feature of Ruby and using it should not draw criticism towards the Rails development team.<br><br>This just my opinion on the matter.

  6. I see a few problems here:

    1. The Ruby development team included an API change (a new method) to a patch release. This should not have been done.
    2. “The nasty habit” of updating and staying on the cutting edge also contributed to this problem. This isn’t just a problem with Avidi, but I have also been bitten by that bad habit.

    3. The Ruby language does not have a well defined spec and test case suit, which leads into problems like these.

    4. Blaming the Rails development team for using a language feature (albeit an often discouraged one) is wrong.

    I offer the following to try to remedy the situation:

    1. Path releases should be just that. No new features should be introduced in a z (as in x.y.z) release.
  7. If one does not need what a new version of any software package provides, there is no need to upgrade. However, it’s an assumed fact in the open source software world that a patch release is, more often than not, safe to upgrade to, so the problem is twofold. (See 1)

  8. There is rubyspec.org and all of the large Ruby based frameworks and libraries have reliable and fairly complete test suites that can be run against a Ruby release. Obviously the Ruby development team failed to test an unusual release with all the available test suites. I don’t have to tell anyone how important testing is, specially when you have pretty a lot of customers.

  9. Thought it is a discouraged practice, for better or worse, the ability to extend the core library (or any already defined class) is a built-in language feature of Ruby and using it should not draw criticism towards the Rails development team.

  10. This just my opinion on the matter.

  11. I'd like to add to comctrl6's comment on the "nasty habit of trying to keep my<br>software up to date". I think this is probably another place where MacPorts<br>has failed you…<br><br>As you know, Ubuntu and Debian (as well as RedHat, SUSE, etc) have the concept<br>of a "stable" release and a "development" or "bleeding edge" release. And the<br>distribution teams take great pains to keep abreast of the various versions of<br>the packages so I don't need to… and only filter the urgent bugfixes or<br>security patches into the stable release. And their stable releases go through<br>several months of QA-induced feature freeze prior to release.<br><br>If I want to venture out on my own with newer versions of certain packages than<br>are in the stable distro, then I take some of that responsibility onto<br>myself… for just those several packages! But by and large, I can trust the<br>distro maintainers to shield me from patch releases that are really minor<br>upgrades in areas that I need stability.<br><br>While there may be no technical reason that MacPorts (and e.g. Gentoo and<br>RubyGems) can't follow this approach… in my experience, they just don't.<br>And that would be fine if I had all the time in the world (in college I always<br>used Debian unstable), but these days I'd rather be getting things done.<br><br>So, while MacPorts' lack of a downgrade path is unfortunate, I would suggest<br>that the bigger problem is that there is no stable upgrade path. And while the<br>distros do sometimes make mistakes with their stable releases, at least they<br>are an additional buffer between me and upstream.

  12. I’d like to add to comctrl6’s comment on the “nasty habit of trying to keep my
    software up to date”. I think this is probably another place where MacPorts
    has failed you…

    As you know, Ubuntu and Debian (as well as RedHat, SUSE, etc) have the concept
    of a “stable” release and a “development” or “bleeding edge” release. And the
    distribution teams take great pains to keep abreast of the various versions of
    the packages so I don’t need to… and only filter the urgent bugfixes or
    security patches into the stable release. And their stable releases go through
    several months of QA-induced feature freeze prior to release.

    If I want to venture out on my own with newer versions of certain packages than
    are in the stable distro, then I take some of that responsibility onto
    myself… for just those several packages! But by and large, I can trust the
    distro maintainers to shield me from patch releases that are really minor
    upgrades in areas that I need stability.

    While there may be no technical reason that MacPorts (and e.g. Gentoo and
    RubyGems) can’t follow this approach… in my experience, they just don’t.
    And that would be fine if I had all the time in the world (in college I always
    used Debian unstable), but these days I’d rather be getting things done.

    So, while MacPorts’ lack of a downgrade path is unfortunate, I would suggest
    that the bigger problem is that there is no stable upgrade path. And while the
    distros do sometimes make mistakes with their stable releases, at least they
    are an additional buffer between me and upstream.

  13. Such problem (adding new reserved words that may break compatibility) in Perl 5.10 was solved be adding new pragma. This pragma is automatically activated when you specify that minimum version of Perl for this module (or main program) is 5.10 or by other means.

Leave a Reply

Your email address will not be published. Required fields are marked *