Rails plugin authors on OS X, beware!

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.

Get In Touch