32 bits of Ruby

April 11, 2011Posted by Tom Meier

A recent rabbit hole that I fell in, related to a rather obscure Ruby 32bit → 64 bit Time parsing issue. I’ll lay it all out here in the hope that no-one else falls into it.

As per usual, all the code was working swimmingly on my own machine (macbook pro – 64 bit with the trimmings, hold the fat), but the minute it was deploying onto multiple servers and the testing environment (see – Jenkins or whatever continuos integration philosophy you employ), a few inserts into the database were chucking nasty “ArgumentError: time out of range” errors.

First picked up when attempting to load fixtures for the test suite. With a bit of hunting this narrowed down to simply one of the date values being set at ‘3000-01-01’ (legacy systems groan), as you can see here:

my 64 bit machine:
1
2
3
4
>> Time.parse('3000-01-01')
=> Wed Jan 01 00:00:00 +1100 3000
>> DateTime.parse('3000-01-01')
=> Wed, 01 Jan 3000 00:00:00 +0000
on 32 bit testing server:
1
2
3
4
5
6
7
8
ree-1.8.7-2011.03 :001 > Time.parse('3000-01-01')
ArgumentError: time out of range
        from /usr/local/rvm/rubies/ree-1.8.7-2011.03/lib/ruby/1.8/time.rb:184:in `local'
        from /usr/local/rvm/rubies/ree-1.8.7-2011.03/lib/ruby/1.8/time.rb:184:in `make_time'
        from /usr/local/rvm/rubies/ree-1.8.7-2011.03/lib/ruby/1.8/time.rb:243:in `parse'
        from (irb):1
ree-1.8.7-2011.03 :002 > DateTime.parse('3000-01-01')
 => Wed, 01 Jan 3000 00:00:00 +0000

So the long and short of it is that Ruby in a 32 bit environment, when running Time.parse tends to have a ‘bit’ of a hissy fit with any years greater than 2038. Time has a limited range of 1901 – 2038, anything, and I mean anything, even a second outside this range, requires DateTime to be used instead.

With my particular issue, I had to track down where it was calling Time.parse, and why the festicky was it not using a superior DateTime.parse call on the input data. Long live the testing suite! A quick change added to the spec_helper.rb:

spec/spec_helper.rb
1
2
3
4
5
6
  #Raise on the method call - REMOVEME
  class Time
    def self.parse(date, now=self.now)
      raise "THIS LITTLE BUGGER CALLED ME : #{caller.inspect}"
    end
  end

On the next run of the relevant spec, which loaded the problematic date times, I could then see the cause of my current despair was none other than Sequel . Sequel has been a fantastic utility, and I’m preferring it over Arel and ActiveRecord for the extra control it gives me. However, this was the root cause.

Turns out Sequel is well aware of the issue and had an immediate fix to hand already, boomshanka!

http://sequel.rubyforge.org/rdoc/classes/Sequel.html

datetime_class [RW] Sequel can use either Time or DateTime for times returned from the database. It defaults to Time. To change it to DateTime:

Sequel.datetime_class = DateTime

For ruby versions less than 1.9.2, Time has a limited range (1901 to 2038), so if you use datetimes out of that range, you need to switch to DateTime. Also, before 1.9.2, Time can only handle local and UTC times, not other timezones. Note that Time and DateTime objects have a different API, and in cases where they implement the same methods, they often implement them differently (e.g. + using seconds on Time and days on DateTime).

Job done. Setting the Sequel default to use DateTime instead of time resolves this issue for both 32 bit and 64 bit machines, and will work on any modern ruby version.

Tagged ruby, rails, programming