We're building a rails app that needs to display dates (and more importantly, calculate them) in multiple timezones.
Can anyone point me towards how to work with user timezones in rails 2.3(.5 or .8)
The most inclusive article I've seen detailing how user time zones are supposed to work is here: http://wiki.rubyonrails.org/howtos/time-zones... although it is unclear when this was written or for what version of rails. Specifically it states that:
"Time.zone - The time zone that is actually used for display purposes. This may be set manually to override config.time_zone on a per-request basis."
Keys terms being "display purposes" and "per-request basis".
Locally on my machine, this is true. However on production, neither are true. Setting Time.zone persists past the end of the request (to all subsequent requests) and also affects the way AR saves to the DB (basically treating any date as if it were already in UTC even when its not), thus saving completely inappropriate values.
We run Ruby Enterprise Edition on production with passenger. If this is my problem, do we need to switch to JRuby or something else?
To illustrate the problem I put the following actions in my ApplicationController right now:
def test
p_time = Time.now.utc
s_time = Time.utc(p_time.year, p_time.month, p_time.day, p_time.hour)
logger.error "TIME.ZONE" + Time.zone.inspect
logger.error ENV['TZ'].inspect
logger.error p_time.inspect
logger.error s_time.inspect
jl = JunkLead.create!
jl.date_at = s_time
logger.error s_time.inspect
logger.error jl.date_at.inspect
jl.save!
logger.error s_time.inspect
logger.error jl.date_at.inspect
render :nothing => true, :status => 200
end
def test2
Time.zone = 'Mountain Time (US & Canada)'
logger.error "TIME.ZONE" + Time.zone.inspect
logger.error ENV['TZ'].inspect
render :nothing => true, :status => 200
end
def test3
Time.zone = 'UTC'
logger.error "TIME.ZONE" + Time.zone.inspect
logger.error ENV['TZ'].inspect
render :nothing => true, :status => 200
end
Processing ApplicationController#test (for 98.202.196.203 at 2010-12-24 22:15:50) [GET]
TIME.ZONE#<ActiveSupport::TimeZone:0x2c57a68 @tzinfo=#<TZInfo::DataTimezone: Etc/UTC>, @name="UTC", @utc_offset=0>
nil
Fri Dec 24 22:15:50 UTC 2010
Fri Dec 24 22:00:00 UTC 2010
Fri Dec 24 22:00:00 UTC 2010
Fri, 24 Dec 2010 22:00:00 UTC +00:00
Fri Dec 24 22:00:00 UTC 2010
Fri, 24 Dec 2010 22:00:00 UTC +00:00
Completed in 21ms (View: 0, DB: 4) | 200 OK [http://www.dealsthatmatter.com/test]
Processing ApplicationController#test2 (for 98.202.196.203 at 2010-12-24 22:15:53) [GET]
TIME.ZONE#<ActiveSupport::TimeZone:0x2c580a8 @tzinfo=#<TZInfo::DataTimezone: America/Denver>, @name="Mountain Time (US & Canada)", @utc_offset=-25200>
nil
Completed in 143ms (View: 1, DB: 3) | 200 OK [http://www.dealsthatmatter.com/test2]
Processing ApplicationController#test (for 98.202.196.203 at 2010-12-24 22:15:59) [GET]
TIME.ZONE#<ActiveSupport::TimeZone:0x2c580a8 @tzinfo=#<TZInfo::DataTimezone: America/Denver>, @name="Mountain Time (US & Canada)", @utc_offset=-25200>
nil
Fri Dec 24 22:15:59 UTC 2010
Fri Dec 24 22:00:00 UTC 2010
Fri Dec 24 22:00:00 UTC 2010
Fri, 24 Dec 2010 15:00:00 MST -07:00
Fri Dec 24 22:00:00 UTC 2010
Fri, 24 Dec 2010 15:00:00 MST -07:00
Completed in 20ms (View: 0, DB: 4) | 200 OK [http://www.dealsthatmatter.com/test]
Processing ApplicationController#test3 (for 98.202.196.203 at 2010-12-24 22:16:03) [GET]
TIME.ZONE#<ActiveSupport::TimeZone:0x2c57a68 @tzinfo=#<TZInfo::DataTimezone: Etc/UTC>, @name="UTC", @utc_offset=0>
nil
Completed in 17ms (View: 0, DB: 2) | 200 OK [http://www.dealsthatmatter.com/test3]
Processing ApplicationController#test (for 98.202.196.203 at 2010-12-24 22:16:04) [GET]
TIME.ZONE#<ActiveSupport::TimeZone:0x2c57a68 @tzinfo=#<TZInfo::DataTimezone: Etc/UTC>, @name="UTC", @utc_offset=0>
nil
Fri Dec 24 22:16:05 UTC 2010
Fri Dec 24 22:00:00 UTC 2010
Fri Dec 24 22:00:00 UTC 2010
Fri, 24 Dec 2010 22:00:00 UTC +00:00
Fri Dec 24 22:00:00 UTC 2010
Fri, 24 Dec 2010 22:00:00 UTC +00:00
Completed in 151ms (View: 0, DB: 4) | 200 OK [http://www.dealsthatmatter.com/test]
It should be clear above that the 2nd call to /test shows Time.zone set to Mountain, even though it shouldn't.
Additionally, checking the database reveals that the test action when run after test2 saved a JunkLead record with a date of 2010-12-22 15:00:00, which is clearly wrong.
After exhaustive research it is now COMPLETELY clear that Time.zone is broken in most all versions of Rails (including 2.3 & 3). This feature uses a central thread hash to store the value that is set (which is supposed to be thread safe, and isn't) and ends up modifying the behavior for all subsequent requests. Additionally, contrary to documentation, setting Time.zone modifies ActiveRecord behavior and saves datetimes in the new zone, instead of the one specified in config (which is usually UTC).
Until Rails gets this fixed, we chose instead to work with Timezones manually, which can be accessed via the undocumented default method:
ActiveSupport::TimeZone['Arizona'].now (or .local, or .parse).
Additionally I patched Time and ActiveSupport::TimeWithZone to provide easy conversion of a time moment to a different zone. To be clear, I mean the corresponding moment of time in a different zone, not the simultaneous moment.
>> Time.utc(2011)
=> Sat Jan 01 00:00:00 UTC 2011
>> Time.utc(2011).in_time_zone('Arizona')
=> Fri, 31 Dec 2010 17:00:00 MST -07:00 #Simultaneous
>> Time.utc(2011).change_zone('Arizona')
=> Sat, 01 Jan 2011 00:00:00 MST -07:00 #Corresponding
The patch is as follows:
module ActiveSupport #:nodoc:
module CoreExtensions #:nodoc:
module Time #:nodoc:
module ZoneCalculations
def self.included(base) #:nodoc:
base.class_eval do
alias_method_chain :change, :zone
end
end
def change_zone(new_zone)
new_zone = ::Time.__send__(:get_zone, new_zone)
return self if new_zone.name == zone
new_zone.local(year,month,day,hour,min,sec,usec)
end
def change_with_zone(options)
result = change_without_zone(options)
options[:zone] ? result.change_zone(options[:zone]) : result
end
end
end
end
class TimeWithZone
def change_zone(new_zone)
time.change_zone(new_zone)
end
def change(options)
time.change(options)
end
end
end
class Time
include ActiveSupport::CoreExtensions::Time::ZoneCalculations
end
You are correct in that Rails does not automatically reset the timezone per request. The wiki author gives an example where the timezone is set in a before filter, thus he never experiences the problem with time zones "leaking" between requests (as the time zone is set up correctly before the request). The example used in the RDoc documentation for TimeZone.zone=
is similar. So I think it's a documentation issue.
Changing the time zone is not request local, it is thread local. Rails stores the selected time zone in the current thread (see Thread.current.[:time_zone]
), not in the current request. As the same thread handles multiple requests, changes to Time.zone are persistent between requests.
I think the proper way to use time zones in the your scenario is to use Time.use_zone
:
def my_action
Time.zone # 'UTC'
Time.use_zone('Mountain Time (US & Canada)') do
Time.zone # 'Mountain Time (US & Canada)'
end
Time.zone # 'UTC'
end
I have not had the time to look through your ActiveRecord issues. Will return with an update when I have done so.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With