Ruby, Rails and MySQL with Leopard 10.5.2 and XCode 3.0

March 19th, 2008

I’m pretty new to Macintosh development but I’ve been working in Ruby for a couple of years under Win32 and Linux. I was excited by the concept of Ruby and Rails being supported “out-of-the-box” on Leopard with the installation of XCode 3.0. But it didn’t take long for the luster to wear off.I first knew things were not going to be easy when I tried to install MySQL. Since I have a recent MacBook Pro with a Core 2 Duo processor, I went for the x86_64 disk image package (ominous background music starts now). It installed without too many difficulties (but not painless w.r.t the system preference pane). I then went to install the C-based ruby mysql bindings gem:

bimota:lib cch1$ sudo gem install mysql
Building native extensions.  This could take a while...
ERROR:  Error installing mysql:	ERROR: Failed to build gem native extension.
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby extconf.rb install mysql
checking for mysql_query() in -lmysqlclient... nochecking for main() in -lm... yes
checking for mysql_query() in -lmysqlclient... nochecking for main() in -lz... yes
checking for mysql_query() in -lmysqlclient... nochecking for main() in -lsocket... no
checking for mysql_query() in -lmysqlclient... nochecking for main() in -lnsl... no
checking for mysql_query() in -lmysqlclient... no
Gem files will remain installed in /Library/Ruby/Gems/1.8/gems/mysql-2.7 for inspection.
Results logged to /Library/Ruby/Gems/1.8/gems/mysql-2.7/gem_make.out

…and thus began the disenchantment.The failure above is caused by the gem command attempting to compile the native MySQL extensions but not being able to find the necessary library and header files. This can be caused by several more fundamental problems:

  1. The path used by default to find the MySQL library files (/usr/local/lib/mysql) does not match the default MySQL installation directory (/usr/local/mysql) on Leopard. This could be remedied by passing command line options to the gem command which would ultimately passed to the configurator (extconf.rb).
  2. The default gem installation tries to build a fat binary with code for four architectures (ppc, ppc64, i386 and i86_64). But, assuming you installed the x86_64 MySQL package, the library file /usr/local/mysql/lib/libmysqlclient.dylib only supports i86_64. This can be remedied either by setting the ARCHFLAGS environment variable before starting the gem command, or by helping the configurator learn the architecture with another command line option.

Both of the above problems can apparently be solved nicely with one command line option that helps the configurator learn the appropriate build options directly from the MySQL installation:

bimota:mysql cch1$ sudo gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
Building native extensions.  This could take a while...
Successfully installed mysql-2.7
1 gem installed

But you would be foolish to believe that it actually works. Run the same command with the verbose option enabled, and you can see that there is trouble on the horizon despite the apparent success:

bimota:mysql cch1$ sudo gem install -V mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
Installing gem mysql-2.7/Library/Ruby/Gems/1.8/gems/mysql-2.7/COPYING/Library/Ruby/Gems/1.8/gems/mysql-2.7/COPYING.ja/Library/Ruby/Gems/1.8/gems/mysql-2.7/README.html/Library/Ruby/Gems/1.8/gems/mysql-2.7/README_ja.html/Library/Ruby/Gems/1.8/gems/mysql-2.7/extconf.rb/Library/Ruby/Gems/1.8/gems/mysql-2.7/mysql.c.in/Library/Ruby/Gems/1.8/gems/mysql-2.7/test.rb/Library/Ruby/Gems/1.8/gems/mysql-2.7/tommy.css/Library/Ruby/Gems/1.8/gems/mysql-2.7/mysql.gemspec
Building native extensions.  This could take a while...
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby extconf.rb install -V mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
checking for mysql_ssl_set()... no
checking for mysql.h... yes
creating Makefile
makegcc -I. -I. -I/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/universal-darwin9.0 -I. -DHAVE_MYSQL_H  -I/usr/local/mysql/include -Os -arch x86_64 -fno-common -fno-common -arch ppc -arch i386 -Os -pipe -fno-common  -c mysql.ccc -arch ppc -arch i386 -pipe -bundle -undefined dynamic_lookup -o mysql.bundle mysql.o -L"." -L"/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib" -L. -arch ppc -arch i386    -lruby -L/usr/local/mysql/lib -lmysqlclient -lz -lm  -lpthread -ldl -lm
ld: warning in /usr/local/mysql/lib/libmysqlclient.dylib, file is not of required architecture
ld: warning in /usr/local/mysql/lib/libmysqlclient.dylib, file is not of required architecture
make install/usr/bin/install -c -m 0755 mysql.bundle /Library/Ruby/Gems/1.8/gems/mysql-2.7/libSuccessfully installed mysql-2.71 gem installed

Indeed, when you go to use (not just ‘require’) the gem, you are likely to see this nasty error:

bimota:mmweb cch1$ rake db:version(in /Users/cch1/Documents/Development/mmweb)dyld: lazy symbol binding failed: Symbol not found: _mysql_init  Referenced from: /Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle  Expected in: dynamic lookup
dyld: Symbol not found: _mysql_init  Referenced from: /Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle  Expected in: dynamic lookup
Trace/BPT trap

I’m still have no idea what causes this specific failure, but I suspect it’s related to the compiler options suggested by mysql_config being ignored/munged by the configurator. The gcc command above tries to build for three different architectures (ppc, i386, x86_64) despite mysql_config’s “recommendation” of just x86_64:

bimota:mysql cch1$ /usr/local/mysql/bin/mysql_config
Usage: /usr/local/mysql/bin/mysql_config [OPTIONS]
Options:
        --cflags         [-I/usr/local/mysql/include -Os -arch x86_64 -fno-common]
        --include        [-I/usr/local/mysql/include]
        --libs           [-L/usr/local/mysql/lib -lmysqlclient -lz -lm]
        --libs_r         [-L/usr/local/mysql/lib -lmysqlclient_r -lz -lm]
        --socket         [/tmp/mysql.sock]
        --port           [3306]
        --version        [5.0.51a]
        --libmysqld-libs [-L/usr/local/mysql/lib -lmysqld -lz -lm]

I then try forcing the issue by setting the environment variable as well:

bimota:lib cch1$ sudo env ARCHFLAGS="-arch x86_64" gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_configBuilding native extensions.  This could take a while...Successfully installed mysql-2.71 gem installed

Again, it looks promising. But now when Rails asks Ruby (1.8.6 from the default XCode 3.0 install) to load the mysql gem, the OS generates a load error due to some kind of a mismatch:

bimota:mmweb cch1$ script/console
Loading development environment (Rails 2.0.2)
>> require_library_or_gem 'mysql'
LoadError: dlopen(/Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle, 9): no suitable image found.  Did find:
	/Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle: mach-o, but wrong architecture - /Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle
	from /Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle
	from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/rubygems/custom_require.rb:32:in `require'
	from /Users/cch1/Documents/Development/mmweb/vendor/rails/activerecord/lib/../../activesupport/lib/active_support/dependencies.rb:496:in `require'
	from /Users/cch1/Documents/Development/mmweb/vendor/rails/activerecord/lib/../../activesupport/lib/active_support/dependencies.rb:342:in `new_constants_in'
	from /Users/cch1/Documents/Development/mmweb/vendor/rails/activerecord/lib/../../activesupport/lib/active_support/dependencies.rb:496:in `require'
	from /Users/cch1/Documents/Development/mmweb/vendor/rails/activerecord/lib/../../activesupport/lib/active_support/core_ext/kernel/requires.rb:7:in `require_library_or_gem'
	from /Users/cch1/Documents/Development/mmweb/vendor/rails/activerecord/lib/../../activesupport/lib/active_support/core_ext/kernel/reporting.rb:11:in `silence_warnings'
	from /Users/cch1/Documents/Development/mmweb/vendor/rails/activerecord/lib/../../activesupport/lib/active_support/core_ext/kernel/requires.rb:5:in `require_library_or_gem'
	from (irb):1
>> ^Dbimota:mmweb cch1$

Why the mismatch? Some googling led me to this post that notes that the Ruby interpreter bundled in XCode 3.0 is in fact only compiled as a 32-bit i386 executable. That’s right: the latest and greatest Macs (with Intel Core 2 Duo processors) running the latest and greatest OS (Leopard 10.5.2) are shipping with a neutered Ruby interpreter.At this point, I see two solutions:1. Install the i386/32-bit only version of MySQL (boo!). I’ve confirmed that this allows a nice working mysql binding to be built with the following command:

bimota:mysql cch1$ sudo env ARCHFLAGS="-arch i386" gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
Building native extensions.  This could take a while...Successfully installed mysql-2.71 gem installedbimota:mysql cch1$

And not only does it compile, it actually works.2. Recompile Ruby with x86_64 support. That sounds like a bigger job -but there are web sites that show you how to get started. For now though, I’m leaving XCode alone until I know what works -that way, when it breaks, I can assign the failure to the 64-bit ruby interpreter.

Validating the presence of exactly one of a list of attributes

November 8th, 2007

I read Eric’s blog on creating an ActiveRecord validation for an exactly-one-of predicate.  I myself had just recently gone through a similar exercise and found a straightforward way of expressing it using the validate method:

validate :exactly_one_of?

def exactly_one_of?
  attrs = [:url, :file, :blob]
  returning (attrs.inject(false){|m, o| m^self[o]}) do |bool|
    self.errors.add(:base, “Exactly one of #{attrs} must be set.”) unless bool
  end
end

While this worked nicely for me, Eric went to the trouble of building a semantically clean validation method that mimics the options of the standard validations.  I prefer using Ruby’s Exclusive OR operator combined with inject instead of the looping in Eric’s method, but I like the semantic style he defined.  Combining his semantics with my logic yields something slick and DRY like this:

def self.validate_exactly_one_of(*attrs)
  options = attrs.last.is_a?(Hash) ? attrs.pop : {}
  configuration = { :message => “one of #{attrs.to_sentence :connector => ‘or’} must be set”, :on => :save }
  configuration.update(options)

  send(validation_method(options[:on] || :save)) do |record|
    returning (attrs.inject(false){|m, o| m^record[o]}) do |bool|
      self.errors.add(:base, configuration[:message]) unless bool
    end
  end
end

I also fiddled the options logic because I think otherwise it would have been possible to have an attribute listed in the error message list that was in fact an option.

Factory methods and scoping with ActiveRecord inheritance hierarchies

October 24th, 2007

Today I ran into a problem with a factory pattern for an ActiveRecord inheritance hierarchy and scoping: has_many association members were being created without references to the “parent” model instance.  For example, my parent class looks something like this:

class Post < ActiveRecord::Base
  has_many :attachments, :as => :attachee
end

In my AttachmentsController, I was trying to create an attachment like this:

@post.attachments.create(attributes)

This approach is bog standard Rails and works fine.  The next iteration involved subclassing the Attachment model class and making Attachment#new method be a smart factory returning an instance of a subclass based on the initialization attributes:

class Attachment < ActiveRecord::Base
  validates_presence_of :attachee_id

  def new(attributes, &block)
    if self == Attachment
      return AttachmentURL if attributes[:url]

      return AttachmentBlob if (attributes[:blob] or attributes[:file])

    end

    super

  end
end

class AttachmentURL < Attachment
end

class AttachmentBlob < Attachment
end

Exercising this code in the console showed the right behavior:

Attachment.new({:url => ‘http://cho.hapgoods.com’})   # => AttachmentURL

But things started looking a bit squirrely when I started to use associations: my validation for the presence of the attachee_id reference was failing:

@post.attachments.create({:url => ‘http://cho.hapgoods.com’})

How could Rails be leaving out the critical association reference?!?  It turns out that association methods like create use :create scoping to set attribute values.  In ActiveRecord::Base.initialize, the :create scope is examined and all scoped attributes are assigned values from the :create scope.  My problem was that the :create scope was being lost when I switched to a subclass in the Attachments#new constructor.  This is the Important Fact:  A scoped ActiveRecord model does not imply scoping for subclasses.

I solved the problem by “transferring” the scope from the base class to the subclasses.  It really wasn’t that complicated and I ended with a really spiffy pattern for subclassed AR model factories.  Here is what my factory method looks like now:

class Attachment < ActiveRecord::Base
  def self.new(attributes, &block)
    if self == Attachment
      klass = AttachmentURL if attributes[:url]
      klass = AttachmentBlob if attributes[:blob] or attributes[:file]
      raise ‘Unknown Attachment type’ unless klass
      klass.send(:with_scope, :create => (scope(:create) || {})) do  # Scope subclass to same create attributes as this class.
        return klass.new(attributes, &block)
      end
    end
    super
  end
end

nota bene: It so happens that I was using Single Table Inheritance (STI) for this hierarchy, but I suspect the problem and solution would be identical with true abstract classes and arbitrary table mapping.  Also, the fact that I was using a polymorphic has_many association is almost certainly irrelevant.

Technorati Tags: , , ,

More on Full Circle REST

September 18th, 2007

Some time ago I frothed on (hip people call this blogging) about how Rails was missing an opportunity to exploit the modelling work done in routes.rb. You can see my post here.

Anyway, in the intervening month or so, I have been working with Ian White’s nifty ResourcesController plugin. As is my wont, I bitch and moan a lot about what is wrong with ResourcesController (RC). The nice thing is that Ian listens and crafts some pretty damn nice code -a lot of my vision of Full-circle REST is starting to appear in RC. In fact, RC is so promising I have abandoned my own plugin and jumped into bed with Ian (in a manner of speaking).

RC can identify many of the RESTful resources of a concrete request with an increasingly concise invocation. It can handle singleton resources, arbitrary nesting and polymorphic nesting. For these features alone, I suggest you look into it.

RC identifies the abstract resources of the URI by examining the inbound request (it has evolved from doing this in a purely textual fashion to examining the matched route’s structure). But it could be better. It could identify the route (when the request arrives) more efficiently. It could link this concrete route directly and unambiguously to the abstract resource that generated it (keeping RC nice and DRY w.r.t routes.rb). And critically, the abstract resource could refer to its parents in the hierarchy of resources in routes.rb, allowing even more direct determination of the complete chain of abstract resources that is being supplied by the URI. Then we could have full-circle REST.

But at this point, RC is hampered by Rails itself. Three patches to edge are required to get us there. I’ve written the first two, and the third shouldn’t be to hard (volunteers?). I’m presenting them in reverse order because I think they’re easier to understand that way.

Patch #3 - Store the hierarchy of abstract REST resources that generate named routes. Currently, only a few strings (name_prefix) provide circumstantial evidence of the parents. Let’s be explicit and store a reference to the the parent in each ActionController::Resources::Resource instance. Then the abstract REST resources will be represented by a forest of trees -and that is pretty close to ideal.

Patch #2 - Store the generating abstract resource in the generated named route. I’ve written the code to do this, but I’m not proud. In fact, I think it is kinda ugly.

module ActionController
  module Resources
      def action_options_for(action, resource, method = nil)
        default_options = { :action => action.to_s, :resource => resource }
        require_id = !resource.kind_of?(SingletonResource)
        case default_options[:action]
          when “index”, “new” : default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements)
          when “create”       : default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements)
          when “show”, “edit” : default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id))
          when “update”       : default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id))
          when “destroy”      : default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id))
          else                  default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements)
        end
      end
  end
end

module ActionController
  module Routing
    class Route
      attr_accessor :resource
    end
    class RouteBuilder      # Construct and return a route with the given path and options (including the resource option)
      def build(path, options)        # Wrap the path with slashes
        path = “/#{path}” unless path[0] == ?/
        path = “#{path}/” unless path[-1] == ?/    

        path = “/#{options[:path_prefix]}#{path}” if options[:path_prefix]

        resource = options.delete(:resource)
        segments = segments_for_route_path(path)
        defaults, requirements, conditions = divide_route_options(segments, options)
        requirements = assign_route_options(segments, defaults, requirements)

        route = Route.new
        route.segments = segments
        route.requirements = requirements
        route.conditions = conditions
        route.resource = resource

        if !route.significant_keys.include?(:action) && !route.requirements[:action]
          route.requirements[:action] = “index”
          route.significant_keys << :action
        end

        if !route.significant_keys.include?(:controller)
          raise ArgumentError, “Illegal route: the :controller must be specified!”
        end

        route
      end
    end
  end
end

The actual code change is minimal, but it is not very clean in RouteBuilder -in fact, that code really should not have been required. FYI, this code was working about a month ago and I assume it should still work. Still, beware!

Patch #1 - Store the recognized route in the abstract request. Testing aside (see below), this one is simple:

module ActionController
  class AbstractRequest
    attr_accessor :recognized_route
  end

  module Routing
    class RouteSet
      def recognize(request)
        params, recognized_route = recognize_path(request.path, extract_request_environment(request))
        request.path_parameters = params.with_indifferent_access
	request.recognized_route = recognized_route
        "#{params[:controller].camelize}Controller".constantize
      end

      def recognize_path(path, environment={})
        routes.each do |route|
          result = route.recognize(path, environment) and return result, route
        end

        allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } }

        if environment[:method] && !HTTP_METHODS.include?(environment[:method])
          raise NotImplemented.new(*allows)
        elsif !allows.empty?
          raise MethodNotAllowed.new(*allows)
        else
          raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}"
        end
      end
    end
  end
end

I’ve changed the return signature of recognize_path so there may be some undesired side-effects. But I haven’t found any yet. Also, the TestRequest object would need to be modified as well to set the recognized_route attribute appropriately because recognize is not invoked in testing (another volunteer?).

So, a quick recap: Rails invites us to model our REST resources (in routes.rb) and uses that information for recognizing inbound routes and generating routes in views. But there is a much value to our modeling work to be found in the controllers themselves. And to harvest that value, we need visibility into the abstract resources that generated the invoked route. Rails needs to evolve in this direction I call full-circle REST.

Technorati Tags:

Why can’t stinit get the attention it deserves?

September 14th, 2007

I’ve been frustrated by tape drives for years.  They are the most finicky, unreliable, least-documented and most expensive components of most of the computer systems I have had the pleasure of administering.  I’m no longer an “up-to-date” sysadmin, but I still try to take care of my own stuff and, not surprisingly, tape drives still are the the top of my bitch list.

Today’s bitch is brought to you by stinit -a great idea that needs some love.

With stinit, you have a means of enforcing a configuration set on a device.  Since each tape drive (in the default Linux st configuration) is represented by four devices, you can easily define four different configuration modes for each drive.  Simply by using a different device (/dev/st0, /dev/st0a, /dev/st0l, /dev/st0m, for example) you can ensure your apps see the tape drive just so.

Unfortunately, stinit is weakly documented (just try the mode= syntax) and full of bugs that make tracking down simple errors costly.  Here’s the one I hit today…see if you can tell what the hell is wrong.

[Background: I have two tape drives connected to this server, one is a Compaq SuperDLT, the other an ancient Archive DDS drive for which I have no documentation]

[root@aprilia amanda]# stinit
Warning: No modes in definition for (’COMPAQ’, ‘SuperDLT1′, ”).
Can’t find defaults for tape number 1.
Initialized 1 tape device.

Hmmm…seems like I left out the modes in the definition of Compaq drive.  Let’s go to the file:


# Archive Python
manufacturer=ARCHIVE model=”Python 28388-XXX” {
        timeout=3600
}

# Compaq SuperDLT1
manufacturer=COMPAQ model=”SuperDLT1″ {
        timeout=3600
        mode1 blocksize=0
        mode2 blocksize=0
        mode3 blocksize=0
        mode4 blocksize=0
}

Puzzling.  The modes are clearly in there.  I try the stinit command again, this time with the verbose option:


[root@aprilia amanda]# stinit -v

stinit, processing tape 0
The manufacturer is ‘COMPAQ’, product is ‘SuperDLT1′, and revision ‘5F5F’.

stinit, processing tape 1
The manufacturer is ‘ARCHIVE’, product is ‘Python 28388-XXX’, and revision ‘4.45′.
Warning: No modes in definition for (’COMPAQ’, ‘SuperDLT1′, ”).
Can’t find defaults for tape number 1.
Initialized 1 tape device.

Interesting that the error message appears after stinit trys to initialize the Archive drive.  After a little googling, I find that an stinit bug was reported in the debian mailing lists (here) for the numbering of the modes.  On a hunch that the programmer couldn’t get his zero-based and one-based numbering straight for the tape drives either, I add a garbage mode for the Archive drive

# Archive Python
manufacturer=ARCHIVE model=”Python 28388-XXX” {
        timeout=3600
        mode1 blocksize=0
}

# Compaq SuperDLT1
manufacturer=COMPAQ model=”SuperDLT1″ {
        timeout=3600
        mode1 blocksize=0
        mode2 blocksize=0
        mode3 blocksize=0
        mode4 blocksize=0
}

…and ABRACADABRA, the problem goes away:

[root@aprilia amanda]# stinit
Initialized 2 tape devices.

Full Circle REST

August 3rd, 2007

The support that Rails has provided for developing RESTful applications is nothing short of amazing considering it was added on long after Rails had an established URL-mapping mechanism (Routes.draw).  But I want more.  Specifically, I want my resources to have a more complete life.

Warning: I am not an expert in Rails or REST, but sometimes I think there has to be a better way.

Background: in the current (late summer 2007) Edge Rails, RESTful resources (ActionController::Resources) are defined primarily for the purpose of creating named routes with a DSL (map.resource …).  A very important by-product of generating named routes is the helper methods available to generate routes in views and controllers.  The net result is a tidy means of generating and recognizing RESTful URIs.  A typical exploitation of these features looks like this:

  1. Define Routes per Resource
  2. Recognize Incoming Request’s Route
  3. (process request in controller)
  4. Generate view, typically with one or more links

Repeat steps 2 through 4 as required.

In step one, the Rails programmer does the hard work: modeling the resources, the interface to those resources, the mapping of resources to controllers, etc.  Rails honors our work and uses these abstract resources to recognize routes (in step two) and to help up generate routes (in step four).  But what about step three?  Can we exploit our abstract resource model here?  Let’s recast the typical methods in a controller that backs a resource:

def index
  identify resource
  manipulate resource
end

def destroy
  identify resource
  manipulate resource
end

def create
  identify resource
  manipulate resource
end

For the index action, the identified resource is a collection represented by a Class or an association.  For the destroy (and show and edit) actions, the identified resource is a member represented by an ActiveRecord model instance.  For the create (and new) actions, the resource is a new, unsaved, AR model instance.  Every controller action that handles a REST request starts with the identification of the resource.  For the following examples, let’s define some resources:

map.resources users do |user|
  user.resources groups, :controller => ‘groups’ do |group|
    group.resources tags, :controller => ‘tags’
  end
end

map.resources groups do |group|
  group.resources users, :controller => ‘users’ do |user|
    user.resources tags, :controller => ‘tags’
  end
end

map.resources vehicles do |vehicle|
  vehicle.resources wheels, :controller => ‘wheels’
end

map.resources unicycles do |unicycle|
  vehicle.resource wheel, :controller => ‘wheels’
end

As these resource definitions show, sometimes resource identification is trivial and sometimes it’s not…

  1. /vehicles/567/wheels
  2. /unicycle/234/wheel
  3. /users/17/groups/2/tags
  4. /groups/2/users/17/tags

In example #1, wheels is a has_many association of a vehicle instance.  It would be nice if Rails helped us find the vehicle instance and pointed us towards its has_many association.  What we get is the params hash, which indicates the vehicle instance, and the invocation of WheelsController.index, which implies the wheels collection association.  Indirect, but adequate.

In example #2, wheel is a has_one association of a unicycle instance.  Rails should help us find the vehicle instance and point us towards the association.  Much as in the first example, Rails gives us a params hash and invokes the WheelsController.show method.  Again, indirect but adequate. 

Examples one and two together illustrate the first ugly problem with resource identification:

Problem One: The arity of resources is implied by the invoked controller method instead of being explicit.

In example #3, tags is a has_many association of a Group instance.  In example #4, tags is a has_many association of a User instance.  The params hash and invoked method  are identical for these two requests.  Rails really lets us down here: short of parsing the request path, there is nothing to distinguish these two requests.

Problem Two: The hierarchy of the request’s resource chain is not preserved.

I believe these two problems stem from the outdated perception of the URL only as a means of triggering a specific controller action and providing some unordered key-value parameters.  In the RESTful world, however, the URL has become a resource specifier.  And that means hierarchy matters and parameters are not limited to key-value pairs.  In a nutshell, Rails needs to help us match a concrete request with our abstract resource model.  Until it does, identification of RESTful resources will continue to be a pain in the ass.

In the meantime, I have taken two steps to help the programmer match an abstract resource to the concrete request by providing some extra information to the controller.  First, I have made the matched route available to the controller as an attribute of the request by monkey patching ActionController::Routing::RouteSet and ActionController::AbstractRequest.  Now I can see the specific components of the request in an abstract way instead of as a string.

But once I had made that change, I realized I was getting very close to the holy grail of the actual abstract resource definition (from routes.rb) used to generate the route.  It was only a couple of more monkey patches before I had the source ActionController::Resources::Resource instance available to the controller as well.

Now in my controllers I can see which of my abstract resources (as defined in routes.rb) matches the incoming request.  That goes a long way towards getting the matching AR model instances instantiated. 

My only remaining beef is that the abstract route definitions (in routes.rb) do not store any meaningful hierarchy information.  In my example above, the subordinate collection resource beneath a group and called ‘users’ is only aware of its parent resource (a group) though a couple of strings (name_prefix and one other).  That means that even when I know the abstract resource that matches the incoming request, I can’t really see its enclosing resources with inferring them from string pattern matching.  Rails should store the parent resource for any nested resources.  That will be my next monkey patch.

Resources
    Jamis Buck’s awesome tutorial on Rails Routing
    Discussion of some of these issues from a different angle
    Group trying to improve REST resource identification

Technorati Tags:

CruiseControl.rb migration: doh!

May 23rd, 2007

I purchased a new build host (Compaq Proliant ML530) off of eBay.  It’s faster and should be more reliable than my previous host (Compaq Proliant 3000 6/550).  After carefully setting up my new build environment with Ruby, Rails, RubyGems, and subversion, I migrated my CC.rb system (two Rails projects and an NSIS project) to the new host.  I ran some builds from the command line and by clicking the “Build Now” button on the web console.  Everything worked well and quickly.  I even changed my CCTray (cool gadget on WinXP) to monitor the new build host and stop monitoring the old host.

Sounds reasonable, right?  Imagine my growing dread as every Rails revision I checked in failed to build.  I would get strange errors in random locations -usually referring to fixture records missing.  And it was taking longer than on the old build host.  I was even more confused when I was able to successfully build the same revisions every time by clicking on the “Build Now” button and by building directly from the command line (in the work directory, ..\build).

Then it hit me: I had left the old build server running, and it was monitoring the same subversion repository and attempting to build as well.  The two build processes were starting at about the same time and walking all over themselves in the test database.

I shut down the old build server and everything started working quickly and nicely.  Doh!

Migration and Virtual Hosts: oops

May 22nd, 2007

I’ve been in the process of migrating the server that hosts this blog to a new machine.  In the process, my blog was down for a while due to my fat-fingering the configuration for my name-based virtual hosting.  The same server that is hosting this blog is also hosting my development site, which includes a couple of Ruby on Rails sites, including one that shares port 80.

Moral of the story: nobody reads my blog, and I need to test better.

Problems with NUT

March 21st, 2007

NUT (Network UPS Tools) has been disappointing me pretty much since the start (two years ago) due to strange shutdowns. Today I decided to do a full analysis.

First, let me describe the configuration: I have two Compaq Proliant 3000 servers, named triumph and bmw, both running Fedora Core 5 with kernel 2.6.20-1.2300.fc5smp. I have two APC Smart-UPS, named cchSU2200 and cchSU1250. They are an APC Smart-UPS 2200 and an APC Smart-UPS 1250, respectively.

Server triumph has two power supplies, each one connected to a different UPS. Server bmw has one power supply, connected to UPS cchSU2200. Both UPSs have their signalling cables connected to serial ports on server triumph. Both UPSs have fresh batteries and “official” APC serial cables.

I won’t go into the details of my upsmon configuration (but will if you ask). Suffice to say that each server requires a minimum of one power supply and monitors all other UPSs (there is actually a third UPS several hundred miles away that is also monitored).

Here are the results and narrative of my testing:

The syslog on triumph:

Mar 22 10:28:26 My Action: Start UPS services
Mar 22 10:28:27 triumph upsd[6734]: Connected to UPS [cchSU2200]: apcsmart-ttyS0
Mar 22 10:28:27 triumph upsd[6734]: Connected to UPS [cchSU1250]: apcsmart-ttyS1
Mar 22 10:28:29 triumph upsd[6735]: Startup successful
Mar 22 10:28:29 triumph upsmon[6738]: Startup successful
Mar 22 10:28:29 triumph upsd[6735]: Connection from 127.0.0.1
Mar 22 10:28:29 triumph upsd[6735]: Client monuser@127.0.0.1 logged into UPS [cchSU2200]
Mar 22 10:28:29 triumph upsd[6735]: Connection from 127.0.0.1
Mar 22 10:28:29 triumph upsd[6735]: Client monuser@127.0.0.1 logged into UPS [cchSU1250]
Mar 22 10:28:36 triumph upsd[6735]: Connection from 192.168.1.10
Mar 22 10:28:36 triumph upsd[6735]: Client monuser@192.168.1.10 logged into UPS [cchSU2200]
Mar 22 10:28:36 triumph upsd[6735]: Connection from 192.168.1.10
Mar 22 10:29:09 My Action: Disconnect power to cchSU1250
Mar 22 10:29:10 triumph upsmon[6739]: UPS cchSU1250@localhost on battery
Mar 22 10:29:11 My Action: Connect power to cchSU1250
Mar 22 10:30:30 triumph upsmon[6739]: UPS cchSU1250@localhost on line power
Mar 22 10:30:54 My Action: Disconnect power to cchSU2200
Mar 22 10:30:55 triumph upsmon[6739]: UPS cchSU2200@localhost on battery
Mar 22 10:30:56 My Action: Connect power to cchSU2200
Mar 22 10:31:03 triumph upsmon[6739]: UPS cchSU2200@localhost on line power
Mar 22 10:34:41 My Action: Disconnect power to cchSU1250 and cchSU2200
Mar 22 10:34:42 triumph upsmon[6739]: UPS cchSU2200@localhost on battery
Mar 22 10:34:42 triumph upsmon[6739]: UPS cchSU1250@localhost on battery
Mar 22 10:34:43 My Action: Connect power to cchSU1250 and cchSU2200
Mar 22 10:35:56 triumph upsd[6735]: Client monuser@127.0.0.1 set FSD on UPS [cchSU2200]
Mar 22 10:35:56 triumph upsd[6735]: Client monuser@127.0.0.1 set FSD on UPS [cchSU1250]
Mar 22 10:36:02 triumph upsd[6735]: Host 192.168.1.10 disconnected (read failure)
Mar 22 10:36:02 triumph upsd[6735]: Host 192.168.1.10 disconnected (read failure)
Mar 22 10:36:02 triumph upsmon[6739]: Executing automatic power-fail shutdown
Mar 22 10:36:02 triumph upsmon[6739]: Auto logout and shutdown proceeding
Mar 22 10:36:07 triumph upsd[6735]: Host 127.0.0.1 disconnected (read failure)
Mar 22 10:36:07 triumph upsd[6735]: Host 127.0.0.1 disconnected (read failure)
Mar 22 10:36:07 triumph logger: upsmon.says.shutdwn

The syslog on bmw:

Mar 22 10:28:35 My Action: Start UPS services
Mar 22 10:28:36 bmw upsmon[9927]: Startup successful
Mar 22 10:29:12 bmw upsmon[9928]: UPS cchSU1250@triumph.hapgoods.com on battery
Mar 22 10:30:32 bmw upsmon[9928]: Giving up on the master for UPS [cchSU1250@triumph.hapgoods.com]
Mar 22 10:30:33 bmw upsmon[9928]: UPS cchSU1250@triumph.hapgoods.com on line power
Mar 22 10:30:58 bmw upsmon[9928]: UPS cchSU2200@triumph.hapgoods.com on battery
Mar 22 10:31:04 bmw upsmon[9928]: UPS cchSU2200@triumph.hapgoods.com on line power
Mar 22 10:34:43 bmw upsmon[9928]: UPS cchSU2200@triumph.hapgoods.com on battery
Mar 22 10:34:43 bmw upsmon[9928]: UPS cchSU1250@triumph.hapgoods.com on battery
Mar 22 10:35:57 bmw upsmon[9928]: Giving up on the master for UPS [cchSU2200@triumph.hapgoods.com]
Mar 22 10:35:57 bmw upsmon[9928]: Giving up on the master for UPS [cchSU1250@triumph.hapgoods.com]
Mar 22 10:35:57 bmw upsmon[9928]: Executing automatic power-fail shutdown
Mar 22 10:35:57 bmw upsmon[9928]: Auto logout and shutdown proceeding
Mar 22 10:36:02 bmw logger: ups.says.shutdown

And now the analysis of the results…

  • On both servers, everything starts normally. The upsmon on bmw is clearly seen to connect to triumph.
  • At 10:29:09, I pulled the power on cchSU1250 and immediately plugged it back in. Immediately, the master upsmon (on triumph) notes the on-battery condition, but I was disappointed to see the amount of time it took to recognize the return to on-line status (over a minute). On bmw, it is worth noting that upsmon “gives up” on the master for cchSU1250 (triumph) right at the end of this wait. This may or may not be a contributing factor to the hideous behavior we are going to observe in a few moments…(ominous background music starts…)
  • At 10:30:54, I pulled the power on cchSU2200 and immediately plugged it back in. The behavior on both triumph (the master) and bmw (a slave) seems perfectly normal, with timely observation of both the on-batter and on-line condition.
  • (ominous background music builds…) At 10:34:41, I pulled the power to both cchSU2200 & cchSU1250 and immediately plugged it back in to both. The master recognize the on-battery conditions, and it propagates them to the slave nicely. But the master never recognizes the immediate return to an on-line condition. The inevitable results appear about a minute later when the master starts shutting down and the slave follows suit (ominous background music reaches a crescendo, and then… silence).

I should point out that this problem has happened numerous times over the past two years. A simple 2 second power glitch will provoke the shutdown of bmw and sometimes even triumph. During that time, I have kept up-to-date with nut through the Fedora yum distribution and upgraded from Fedora Core 4 to Fedora Core 5. Currently, I am running nut version 2.0.3, release 0.1.fc5. The APC Smart protocol driver claims to be version 1.99.7 with a command table version of 2.0.

Any ideas? I ran some more auxiliary experiments and found that while upsmon seems to take a long time to recognize a return to on-line in cchSU1250, upslog reports the OL condition immediately. So I think the communication between the UPS and upsdrvctl is working.

Does anybody really know what time it is?

March 12th, 2007

Ruby’s built-in Time class is weak -it has no notion of time zones other than the OS-supplied “UTC” and “local” zone contexts. To compound matters, Rails’ independent TimeZone class is flawed, supporting an offset from UTC but no DST rules or flag. Using the standard tools is an exercise in frustration. Here is how to improve the situation:

 

 

Step One: Install the TZInfo gem. This will provide a comprehensive OS-independent time zone database that includes DST data. Apparently this data is kept pretty current, as it knows about the exceptional changes to US time zones introduced in Spring, 2007. At this point, you can associate a legitimate time zone with a user, for example (see reference one). But you still need to convert instances of Time from one timezone to another -typically from UTC to the user’s timezone. And worse, you need to track the time and the relevant timezone independently.

 

Step Two: Force all normal Ruby and Rails operations to consistently use UTC. This avoids the situation where the flawed Ruby and Rails standard libraries attempt to “help out” and end up mucking up the works. In environment.rb:

 

 

 

 

require_gem ‘tzinfo’

include TZInfo

ActiveRecord::Base.default_timezone = :utc

ENV[’TZ’] = ‘UTC’

 

 

 

 

 

 

 

 

Unfortunately, setting TZ doesn’t seem to work reliably under Win32. Behavior is very sensitive to where and when the variable is set.

Step Three: Overwrite the Rails TimeZone class with an improved one that uses the gem from step one by installing the tzinfo_timezone plugin. Now the standard time zone methods such as now() will use the DST-aware TimeZone data.

Before:

  • TimeZone[’Eastern Time (US & Canada)’].now => Mon Mar 12 07:15:17 +0000 2007
  • TimeZone[’Eastern Time (US & Canada)’].now => Mon Mar 12 08:18:52 UTC 2007

After:

  • Timezone.get(’America/New_York’).now => Mon Mar 12 08:18:52 UTC 2007

In late March 2007, the “Before” result is incorrect as DST is in effect. The “After” section shows how the plugin has made the standard Rails TimeZone heel to DST.Note that all results appear to be in UTC because the return value (an instance of Time) is incapable of storing times in arbitrary timezones.The best solution seems to be to always claim the time is in UTC. I’ll emphasize this point because it is so counter-intuitive: the tzinfo_timezone plugin and other similar plugins generally ignore the Ruby Time class’ time zone on input and output inconsistent Time instances (where the time is in a timezone x but the output shows UTC) -this is normal in the perverse world of Ruby time.

 

 

 

 

Strangely, TzinfoTimezone will only accept identifying strings from the standard Rails time zones (e.g. Eastern Time (US & Canada)); there is no support for the native TzinfoTimezone identifiers (e.g. America/New_York). Internally, it clearly uses the TzinfoTimezone class to store the timezone, but the lookup method uses the standard Rails table and its naming convention. I’ve requested relief from the author, but in the meantime, a patched version is available from my svn repository: svn://cho.hapgoods.com/tzinfo_timezone

 

Step Four: Install the tztime plugin.This adds support for time values that are aware of their frame of reference (a TZInfo time zone).Better yet, it quacks like a Time value and can be used anywhere a time value would be used.Consequently, you no longer need to carry time zone values around right up to the point of generating output. The results speak for themselves:

Before:

  • Timezone.get(’America/New_York’).now => Mon Mar 12 08:18:52 UTC 2007

After:

  • <set the “local” timezone>
  • TzTime.now => 2007-03-12 08:18:52 EDT

 

 

 

Ignoring the formatting of times, the new TzTime type clearly knows what is going on. It is aware of its time zone to the point we can do the following:

 

 

TzTime.now.dst? => true

TzTime.now.utc.dst? => false

 

 

Setting the local timezone is important to the usage of TzTime to ensure times are output with the proper timezone.Without a reference time zone, TzTime will generate errors. Reference three (the definitive reference for this plugin) shows how to set the local timezone in an around filter. Here are some examples:

 

 

TzTime.zone = TimeZone[’Eastern Time (US & Canada)’]

TzTime.zone = current_user.tz

 

 

 

 

Thanks to Jamis Buck for writing these two plugins. With his initiative, Rails moves one step forward towards being an ideal platform for internationalized applications.

 

 

References:

  1. http://marklunds.com/articles/one/311
  1. http://tzinfo.rubyforge.org/doc/
  1. http://weblog.jamisbuck.org/2007/2/2/introducing-tztime