method_missing: best saved for last

If you read this blog you know I’m a huge fan of Ruby. I have nothing but good things to say about it. I’m dangerously close to being labeled a fanboy. For the longest time I thought I’ll eventually get to redeem myself by linking to an article pointing out some of the downsides of Ruby. Little did I know, I’ll end up writing it.

You may ask yourself, why I’m so pessimistically looking for the downfall of a language I’m invested in? I’m not. I’m looking for gotchas.

Perfect only exists on TV. Every piece of software you work with has warts, gotchas and, unfortunately, sometimes blackholes through which all your energy goes and none comes back. Part of writing software is knowing which pitfalls to avoid, which trapdoors to walk around, and how to navigate around strong centers of gravity. The quicker I build that knowledge, the better tool it becomes.

One thing that always concerned me was meta-programming. Java doesn’t have much of a heritage for meta-programming. Very few brave through thick forests of reflectors, proxies and code enhances, even less come out the other side. Ruby on the other hand, in instrument of precision that can cut through cruft like nobody’s business. But remember what they say, and don’t go running with scissors.

There’s also LISP et al, but these languages always struck me as surgical scissors. Ultimate precision that are painful to use on cardboards, fabrics and the odd electrical cord. I never managed to use those in real life to learn any real lesson.

Try as I may, I never managed to shoot myself in the foot, but I’m usually careful like that. Until today.

method_not_missing

I’m working on a project right not that uses SFTP to upload files onto a remote server. I point the code at the base URL, essentially the root document directory, and let it upload all the files over. The files are set in a structure, so the code creates those remote directories before it can upload the files. In spite of the non-existent documentation, I managed to grok Net::SFTP quickly, part from peeking at the Capistrano source code, and part from being based on Net::SSH.

So now I’m setup and ready to go, I run the first test and it passes successfully. The second run fails, complaining the directories already exist, so I use realpath to check before using mkdir to create. Repeatable runs are successful, as I’m adding progress bar, checksums, cleaning up the code, and testing after every major change. So far so good.

At this point the code is running in a “test” configuration, uploading files to localhost, which like every good Linux box comes pre-installed with SSH/SFTP. When I try to use it against the production server, it fails. Somehow, Net::SFTP still insists on creating directories on localhost, and fails uploading the files because it’s missing these directories on the remote server. It’s as if Net::SFTP retained the memory of the old configuration, and I can’t shake it loose.

So I let it fail and check the stack trace, followed by a long session of grokking Net::SFTP code and a few more experiments to nail it down.

Turns out that Net::SFTP does not implement the mkdir method. Instead, its Session object waits for method_missing to catch the call and redirects the request to the underlying driver. Driver doesn’t implement mkdir either, again using method_missing to redirect the call to the underlying protocol implementation. There are five of those, one for each version of the SFTP protocol, so I go over them in order.

That part of the design is ingenious: how Net::SFTP behaves depends on which version of the protocol it negotiated with the server. It’s a lightweight plugin architecture. But it’s not the source of my problems, mkdir is implemented since version 1 of the protocol.

The problem turns out to be the combination with Rake.

Rake assumes that most of your objects are rake task, or somehow deal with tasky stuff. To make your life easier, and boy is it easier to use than Ant, Rake throws a lot of enhancements into the Object class. You can define a task from any object, which means in any context. For good measure, it also lets you use FileUtils methods from any context, er, object.

And right there was the problem staring me in the face. Because Rake adds mkdir to any object, calling sftp.mkdir routes the request straight to FileUtils, and never falls back on method_missing. The code works locally because it creates the directories through the file system, not the SFTP protocol.

One of those bugs that pass with flying green in the test cases, and fail miserably in production.

Once I figured it out, it was easy to solve. I monkeypatched Session and Driver to implement the mkdir method that would simply forward the request straight to method_missing. One meta-programming trick to solve another.

Try it yourself

Picking a host (anything but localhost) and a path you can access, try the following from a regular Ruby file:

Net::SSH.start(host) do |session|
  session.sftp.connect do |sftp|
    sftp.mkdir path
  end
end

Next, try running the same code from a Rakefile, and watch as it creates the path on the same machine.

To prove that the fix works, add this (stubbing only Session or only Driver will not work):

class Net::SFTP::Session
  def mkdir(path, attrs = {})
    method_missing :mkdir, path, attrs
  end
end

class Net::SFTP::Protocol::Driver
  def mkdir(first, path, attrs = {})
    method_missing :mkdir, first, path, attrs
  end
end

Peoplepatching

It’s great that I can monkeypatch Net::SFTP to do what I want, but more interesting to me is learning how to not inflict the same problems on my own code. So I came up with some lessons learned, and easy rules to follow.

Three for now, if you have more to suggest, I’m all ears.

  1. Use method_missing sparingly. Defining methods is easy enough, so do it as often as possible, and let method_missing deal with hardcore trickery. As a general rule, if you can’t look at the code and figure out which methods it provides, reconsider how to implement them.
  2. Use declarative constructs. Unfortunately, Ruby lacks in this department, it’s sometimes impossible to tell how classes and objects are extended. On the other hand, Rails, Facets and other projects have meta-programming methods that are much more descriptive and meaningful. Use those as often as possible. Where Rake redefines Array methods into FileList, it would be better if it included ActsAsArray.
  3. Document that which you extend. And no, burying the text somewhere inside the code doesn’t count as documenting. You want it up there at the top of the RDoc, where your eyes are scanning the list of defined and inherited methods.

Now, excuse me while I go and check my old code over again, and get rid of some rogue method_missing and patch the documentation.

Photo by michael gallacher

16 thoughts on “method_missing: best saved for last

  1. Pingback: Auto Moving Service » Reconsider method_missing

  2. A wise, cautionary tale, but do you think that had you stubbed sftp’s mkdir this problem could have been avoided? It appears you’re writing your tests against the filesystem.

  3. I once had a Professor who delivered to me the following wisdom: “One doesn’t doesn’t really know a tool unless it has been used in anger”

  4. Pingback: Chipping the web - Krushchev’s due at Idlewild! -- Chip’s Quips

  5. Chris,

    I edited the post and added a sample of the code that fails, and the stubbing I used to solve it. You can try it out yourself.

    The only reason it worked during testing is that I used a single machine. I pointed Net::SFTP at localhost, and then checked the changed to the file system. The code was definitely running against Net::SFTP, only checked against the file system.

  6. I think the real issue is Rake polluting Object with a bunch of FileUtil stuff.

    Using method_missing to dispatch to underlying implementations is fine, especially if you don’t know what methods those implementations will support. See for example the EVDB Ruby API client. My choice would be to use explicit delegation whenever I know the methods I’m expecting to use, but Net:SFTP isn’t changing anytime soon.

    And I think the other Chris meant something else by stubbing, than the sense in which you used it. Maybe the term mocking is more appropriate for his usage. The basic idea being that if the SFTP implementation expects :mkdir, then your tests won’t satisfy the expectation. Checkout RSpec and Mocha, both sweet mocking frameworks.

  7. Chris,

    You’re right, Net::SFTP works well everywhere else, it’s only Rake (that I know of) that breaks it.

    But when something breaks you have to fix it. I’m trying to learn by example so I can create code that — when it breaks, and most likely it would — will be easy to fix.

    Figuring out what Rake does was easy, I just looked at the docs (document that which you extend). Figuring out what Net::SFTP is doing took a long while, digging through its code in search of the elusive mkdir that doesn’t exist, and then having to understand how it works to figure out the two patches.

    And thanks for pointing out the confusion about stubbing.

    I use RSpec for the test suite, it’s awesome and I love writing tests with it. The tests I’m referring to here were done by running the code against a live server and seeing what happens, so no mocking or stubbing involved.

  8. Pingback: Labnotes » Rounded Corners - 103

  9. Interesting post. Jim Weirich (author of Rake) gave a talk at RailsConf 2007 entitled “Playing it safe” which was all about how to design libraries so they don’t interfere unexpectedly with other libraries. He illustrated his talk with things he’d done in Rake which he now regrets!

  10. I agree with Chris here: the problem is Rake monkeypatching Object, which it has no business touching.

    Monkeypatching seems like a serious illness of the Ruby community; unlike the stodgy Java types, I firmly believe it is essential to be able to do it, but I also firmly believe it’s a strategy of last resort. Last resort. And it should never ever be done by a library or framework; only by application code which is standalone and won’t be mixed with other code down the line, or as part of a metaprogramming abstraction. Otherwise, you get clashes and interactions like this one.

    method_missing, in contrast, is perfectly fine. I use it quite frequently without any ill effects (or rather its equivalent in my preferred language). In fact, it’s a prime enabler of OO metaprogramming; things like proxies and mock objects are impossible without it.

  11. A word about terminology: If you call it “monkeypatching” then you probably do not understand it well enough to be using it.

  12. Pingback: » Putting the client before the technology, by putting the right technology before the client | IT Consultant | TechRepublic.com

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>