Picture of stu

Rails plugin authors on OS X, beware!

  • Posted By Stuart Halloway on January 31, 2008

This morning I was troubleshooting a production problem with the simple_localization plugin. The code worked fine in development, had 100% passing C0 coverage in test, and worked fine in production on my local box. But on the staging box, we were getting the dreaded load error:

LoadError: Expected /simple_localization/lib/cached_lang_section_proxy.rb to define CachedLangSectionProxy

If you use Rails plugins and ever see this problem, read on...

A little background

In Ruby, you can load a Ruby source file from the load path by requiring it.

require 'my_class'

This is explicit, and easy to understand. But you might get tired of spelling things out all the time. So in Rails you can also load a class implicitly when it is needed:

MyClass

This is somewhat Java-like, in that magic happens to find the code based on some naming conventions, e.g. My::Namespaced::MyClass should be in a file namedmy/namespaced/my_class.rb somewhere on the load path. It is also Java-like in being difficult to debug, leading to errors like the LoadError above.

Workaround: ducking the issue

Knowing that the LoadError is a failed implicit load, the first step is to look at the point of failure in the file cached_lang_section_proxy. Here is is, elided for clarity:

module ArkanisDevelopment
  module SimpleLocalization
    class CachedLangSectionProxy

Ah hah, you say. The error is right on. This file doesn't define CachedLangSectionProxy, it defines CachedLangSectionProxy in the ArkanisDevelopment::SimpleLocalization module. So implicit loading can't work with the code as written. But we have a workaround: we can move this file (and probably several others) into a directory structure that matches Rails conventions. I am not going to do that, because...

Solution: getting deterministic

We can get implicit loading to work, but we still haven't tackled the real problem. Why did the code ever work on my local box to begin with? We know that implicit loading can't work, so somehow my local box must be explicitly loading the files, but in a machine-dependent way that fails on the staging box.

Rails plugins include an init.rb that runs during Rails startup, and is often used to explicitly load configuration and code. Here is that code from simple_localization:

Dir[File.dirname(__FILE__) + '/lib/*.rb'].each do |lib_file|
  require File.expand_path(lib_file)
end

This is broken, but if you develop on Mac OS X you may never notice. The plugin's internal dependencies are arranged in such a way that loading the files in alphabetical order works. In all of my experiments, Ruby's directory traversal APIs on the Mac return files in alphabetical order. However, this ordering is not required by the Ruby language. On Linux, the files can come back in any order.

Given that many Rails developers work on OS X, and deploy to Linux, this leads to an amusing variant of "It works on my box": It works on all developer boxes, and fails on all production boxes..

An easy fix is to sort the files explicitly:

Dir[File.dirname(__FILE__) + '/lib/*.rb'].sort.each do |lib_file|
  require File.expand_path(lib_file)
end

Better would be to organize init.rb so that the dependencies are clear (the fact that alphabetical order happens to work is a fragile coincidence).

Lessons learned

  1. If you write Rails plugins on Mac OS X, be careful how you use globbing APIs in init.rb. They will work deterministically on your box, but maybe not everywhere else.
  2. If you plan for your Ruby code to be used from Rails, follow the directory and naming conventions.
  3. Loading code is and always will be tricky. Many years ago, I thought that COM had solved many of the problems. I was so enthusiastic about Java's approach that I wrote a book about it. By the time .NET came out with yet another approach, I was a bit jaded and assumed it would have problems. (It did.) It's a hard problem.

On language aesthetics

Java and Ruby both have an explicit and implicit loading story. What is interesting is that in Java this story is implemented in the language, while in Ruby a significant part of the story is in the libraries. It is Rails, not Ruby, that implements implicit loading, and you can read much of that story in this source file (updated link: with syntax highlighting). Understand this file, and you will know much of what is best and worst in Ruby.

Comments
  1. choonkeatFebruary 01, 2008 @ 12:57 AM

    The exact issue was fixed (for some other parts of Rails) at http://dev.rubyonrails.org/ticket/6178 a year ago.

    Seeing it happen again makes me think its a language “problem”. What would be the good fix for this? Modifying Dir[] to be sorted?

  2. Rob SanheimFebruary 01, 2008 @ 02:16 AM

    Dependancies in Rails can be one of the most hideous things, and its further complicated by the fact that reloading changes the whole story in development, and may result in weirdness not seen in production.

    Also, here’s a link to the source, but via trac, so it has syntax highlighting and is pretty:

    http://dev.rubyonrails.org/browser/trunk/activesupport/lib/active_support/dependencies.rb

  3. Stephan SollerFebruary 01, 2008 @ 03:49 PM

    Thanks for this good blog post. I’m the author of the Simple Localization plugin and I think this is the most in depth bug analysis I got so far. :)

    Usually I avoid all this by requiring the needed files explicitly by hand. So I did in this plugin and strictly this Dir[] stuff isn’t needed at all for loading. It’s just necessary to load tree or four source files and these are then explicitly loading all their dependencies. However I used this Dir[] call for more flexibility during development.

    btw.: I’m developing on Ubuntu Linux and deploying on Debian Linux so I can confirm that it works there, at least with Debian based systems. What Linux, Unix or BSD based server are you deploying on?