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...
In Ruby, you can load a Ruby source file from the load path by requiring it.
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:
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 named
my/namespaced/my_class.rb somewhere on the load path. It is also Java-like in being difficult to debug, leading to errors like the
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...
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).
init.rb. They will work deterministically on your box, but maybe not everywhere else.
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.