activerecord and timezones - ruby-on-rails

Here's the situation. We have a rails 3.2 app running on heroku, and developers all over the world. We want to standardise on UTC for everything. Our team speaks UTC even among ourselves (eg. when planning meeting times). We want the app to do the same. We can convert things back into people's local timezones for display at some later point.
The problem is that when I save something into the database (for instance, creating a new post in the forums) it thinks that my local time (as reported by my computer) is UTC, and saves it as such.
For example, it's lunchtime on Thursday 14th February here in sunny Melbourne, Australia (UTC+11). I make a new post, then look in the console:
1.9.3p194 :009 > p = Post.first
=> #<Post id: 12, author_id: 1, subject: "Another post ", body: "a very recent one!", created_at: "2013-02-14 12:13:53", updated_at: "2013-02-14 12:13:53", slug: "test1-20130214-another-post", forum_id: 1>
1.9.3p194 :010 > p.created_at
=> Thu, 14 Feb 2013 12:13:53 UTC +00:00
As you can see, the created_at timestamp is saved as per lunchtime on the 14th, and flagged as being that time IN UTC. What I actually want is for it to look at the local time, be aware that I'm in Melbourne, and convert to UTC by (in this case) subtracting 11 hours.
Now, I know I can edit these lines in my config/application.rb:
config.time_zone = 'UTC'
config.active_record.default_timezone = 'UTC'
(I think changing config.time_zone to 'Australia/Melbourne' would fix it? Probably?)
However, if I change any of that to "Australia/Melbourne" (or whatever), then what happens to our deployed app (on Heroku), and my co-developers in Maryland and Oregon and Glasgow? Do we all have to set our times individually? Should we do it via environment variables, perhaps? Any other suggestions?

OK, so it looks like this fixes it for us:
config.time_zone = 'UTC'
config.active_record.default_timezone = :local
We're a little dubious and will wait and see how it goes when we deploy and/or run our test suite in everyone's timezones, but for now it seems like it works... in Melbourne at least ;)

Related

Why does Date.yesterday() return the day before yesterday in rails?

I am a little bit confused by the Date.yesterday(), for example:
$ rails c
Loading development environment (Rails 6.0.3.2)
irb(main):001:0> Date.today
=> Fri, 10 Jul 2020
irb(main):002:0> Date.yesterday
=> Wed, 08 Jul 2020
irb(main):003:0> Time.now
=> 2020-07-10 03:54:46.02207138 +0530
irb(main):004:0>
But if I am not wrong, if today is Friday, which is true, the previous day should be Thursday as I learnt in my primary school..
What's going on in here?
tl;dr: Use Date.current and Time.current instead of Date.today and Time.now.
Your application has its own time zone, Time.zone. Ruby is not aware of time zones, but Rails is. Rails partially updates Time and Date to be aware of time zones, but not completely. Any methods Rails adds will be time zone aware. Date.yesterday and Date.tomorrow, for example. Built-in Ruby methods it leaves alone, like Date.today. This causes some confusion.
Date.today is giving today according to your local time zone, +0530. Date.yesterday is giving yesterday according to your application's time zone which I'm guessing is +0000 (UTC). 2020-07-10 03:54:46 +0530 is 2020-07-09 22:24:46 UTC so Date.yesterday is 2020-07-08.
Use Date.current instead of Date.today. Date.yesterday is a thin wrapper around Date.current.yesterday. Similarly, use Time.current instead of Time.now.
The ThoughtBot article It's About Time (Zones) discusses Rails time zones in detail and has simple DOs and DON'Ts to avoid time zone confusion.
DON'T USE
Time.now
Date.today
Date.today.to_time
Time.parse("2015-07-04 17:05:37")
Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z")
DO USE
Time.current
2.hours.ago
Time.zone.today
Date.current
1.day.from_now
Time.zone.parse("2015-07-04 17:05:37")
Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z").in_time_zone
Make sure to check the location of the server where the application is running. The value of Date.yesterday will likely depend on the location of the server with respect to the international date line. For example, while it is still Thursday, 07/09/2020 in New York City, it is Friday, 07/10/2020 in New Zealand.
In your Rails app, you may have a value set for Time.zone or in the configuration for config.time_zone.
If one of these values is configured, then Rails uses this as follows:
def yesterday
::Date.current.yesterday
end
def current
::Time.zone ? ::Time.zone.today : ::Date.today
end
These are ActiveSupport Date helpers
So it's indeed a result of wrong Timezone, in my case Time.zone.name returned "UTC". The correct zone for me is Asia/Kolkata, to check the presence of the timezone:
ActiveSupport::TimeZone::MAPPING.select { |x| x[/^kol/i] }
Which returned {"Kolkata"=>"Asia/Kolkata"} in my case.
For it to take effect temporarily, I set Time.zone = 'Kolkata' in the rails console.
For a permanent effect, added config.time_zone = 'Kolkata' to {project_root}/config/application.rb.
This fixed the problem, and Date.yesterday(), Date.today(), and Date.tomorrow() are working as expected.

Rails Upgrade makes default Time format change. How to revert?

I am upgrading a Rails app from
Rails 4.2 -> 5.2 (a subsequent upgrade to Rails 6 is pending)
Ruby 2.2 -> 2.5
Postgres 9.1 -> 10
in various steps. Since the Rails upgrade requires the Postgres upgrade I can't separate the upgrades in a sensible way.
Currently I am struggling with the way "Time" objects are handled in Rails 5.2. A "time" column in an AR object is now returned as an ActiveSupport::TimeWithZone, even if the database column has no time zone. Previously it was a plain Time object which had a different default JSON representation.
This makes a lot of API tests fail which were previously all returning UTC times.
Example for Rails 4.2, Ruby 2.2, PG 9.1 for a PhoneNumber object:
2.2.6 :002 > p.time_weekdays_from
=> 2000-01-01 07:00:00 UTC
2.2.6 :003 > p.time_weekdays_from.class
=> Time
Example for Rails 5.2, Ruby 2.5, PG 10:
irb(main):016:0> p.time_weekdays_from
=> Sat, 01 Jan 2000 11:15:00 CET +01:00
irb(main):018:0> p.time_weekdays_from.class
=> ActiveSupport::TimeWithZone
I have added an initializer to override this for the time being and this seems to work fine, but I'd nevertheless like to understnand why this change has been made and why even 'time without time zone' DB columns are being treated by Rails as if they had a timezone.
# This works, but why is it necessary?
module ActiveSupport
class TimeWithZone
def as_json(options = nil)
self.utc.iso8601
end
end
end
PS: I don't always want UTC, I just want it for the API because that's what our API clients expect.
Currently I am struggling with the way "Time" objects are handled in Rails 5.2. A "time" column in an AR object is now returned as an ActiveSupport::TimeWithZone, even if the database column has no time zone. Previously it was a plain Time object which had a different default JSON representation.
I'd nevertheless like to understnand why this change has been made and why even 'time without time zone' DB columns are being treated by Rails as if they had a timezone.
This change was made because Ruby's default Time has no understanding of time zones. ActiveSupport::TimeWithZone can. This solves a lot of problems when working with times, time zones, and databases.
For example, let's say your application's time zone is America/Chicago. Previously you had to decide whether you're going to store your times with or without time zones. If you opt for without a time zone, do you store it as UTC or as America/Chicago? If you store it as UTC, do you convert it to America/New York on load or on display? Conversion means adding and subtracting hours from the Time. When you save Time objects you have to be careful to remember what time zone the Time was converted to and to convert it back to the database's time zone. Coordinating all this leads to many bugs.
Rails 5 introduces ActiveSupport::TimeWithZone. This stores the time as UTC and the desired time zone to represent it in. Now handling time is simpler: store it as UTC (ie. timestamp) and add the application's time zone on load. No conversion is necessary. Rails handles this for you.
The change is now timestamp columns, by default, will be formatted in the application's time zone. This takes some getting used to, but ultimately will make your handling of times and time zones more robust.
> Time.zone.tzinfo.name
=> "America/Chicago"
> Time.zone.utc_offset
=> -21600
# Displayed in the application time zone
> Foo.last.created_at
=> Tue, 31 Dec 2019 17:16:14 CST -06:00
# Stored as UTC
> Foo.last.created_at.utc
=> 2019-12-31 23:16:14 UTC
If you have code which manually does time zone conversions, get rid of it. Work in UTC. Time zones are now just formatting.
As much as possible...
Work with objects, not strings.
Work in UTC, time zones are for formatting.
If you need to turn a time into a string, make the formatting explicit.
def get_api_time
'2000-01-01 07:00:00 UTC'
end
# bad: downgrading to strings, implicit formatting
expected_time = Time.utc(2000, 1, 1, 7)
expect( get_api_time ).to eq expected_time
# good: upgrading to objects, format is irrelevant
expected_time = Time.zone.parse('2000-01-01 07:00:00 UTC')
expect(
Time.zone.parse(get_api_time)
).to eq expected_time
# better: refactor method to return ActiveSupport::TimeWithZone
def get_api_time
Time.zone.parse('2000-01-01 07:00:00 UTC')
end
expected_time = Time.zone.parse('2000-01-01 07:00:00 UTC')
expect( get_api_time ).to eq expected_time
I recommend reading these articles, they clear things up.
It's About Time (Zones)
The Exhaustive Guide to Rails Time Zones

Rails 3 timezone confusions

I'm confused on how rails 3 timezones are supposed to work.
So I config rails to use Pacific time, and tell active record to store in Pacific time.
# application.rb
config.time_zone = 'Pacific Time (US & Canada)'
config.active_record.default_timezone = 'Pacific Time (US & Canada)'
Now I submit update a model and this comes through in the params:
"start_at"=>"2013-07-24 00:00:00"
From the console now:
>> Sale.last
=> #<Sale id: 24, start_at: "2013-07-24 00:00:00", ...snipped... >
>> Sale.last.start_at
=> Tue, 23 Jul 2013 17:00:00 PDT -07:00
>> Sale.last.start_at.in_time_zone
=> Tue, 23 Jul 2013 17:00:00 PDT -07:00
So after trying to force everything to Pacific time, its creating time objects form the database by factoring in the -7 hours of Pacific time.
If I set a time to 2013-07-24 00:00:00 I would expect Tue, 24 Jul 2013 00:00:00 PDT -07:00 to come back out. And yet it does not. I was having similar confusing issues when active record was using UTC to store dates. I had a few tests that would fail only after 5pm when time to date conversions yielded a different date.
How do I tame this? Storing UTC dates in the database seems like a good idea, and I can use in_time_zone on time objects for display, but does that means that times in forms must be UTC?
Our application functionality is very tied to server time, and certain thing happen every day as specific times. So forcing everything to Pacific time seems like it should be fine. But so far I can't seem to make this behave consistently.
How do I make all this not suck?
This is going to be a 1/2 answer, re-iterating from comment thread above with some additional information
I hope to update with more later. There are many gotchas with this stuff
UPDATE: finally did a blog post on rails timezones
http://jessehouse.com/blog/2013/11/15/working-with-timezones-and-ruby-on-rails/
See also: http://www.elabs.se/blog/36-working-with-time-zones-in-ruby-on-rails
i would use UTC for the application timezone and the activerecord default. Use I18n.localize method (setup formats in config/locales/en.yml) for display of dates and datetimes; set the current threads timezone based on user settings or a default (whatever makes sense for your app)
if the current thread is set to the users timezone activerecord should do the right magic and save the offset UTC value in the db, then when you display that value to the user with I18n it will display it as the user entered it (converting from UTC to the users timezone) - it gets a bit tricky when you are entering a time for say an event that is taking place in another timezone - in that case the thread needs to be set to the timezone of that location and using I18n displays need to be for the location instead of the user
setting current users timezone
see http://railscasts.com/episodes/106-time-zones-revised which uses around filter
example below uses before_filter
both assume a time_zone column on the user account
some code
class ApplicationController < ActionController::Base
protect_from_forgery
# auth the user
before_filter :authenticate_user!
# Set user's time zone
before_filter :set_user_time_zone
# ....
def set_user_time_zone
if current_user and current_user.time_zone.present?
Time.zone = current_user.time_zone
# else some default?
end
end
end
Alternatively - set based on browser settings, etc...
see http://guides.rubyonrails.org/i18n.html
specifically: http://guides.rubyonrails.org/i18n.html#setup-the-rails-application-for-internationalization
display date times
Use I18n.localize aka I18n.l or just l - see http://guides.rubyonrails.org/i18n.html#adding-date-time-formats
changing the timezone display for part of a view
User is set to Pacific but showing an event time for an event taking place in Eastern
Time.use_zone(event.location.time_zone) do
puts event.start_time
end
WARNING: I have found the above does not work correctly if event was pulled using find_by_sql method, regular active record queries work well

Problems with saving and querying dates in Rails

I have an Appointment model, and for one particular appointment, I saved it having a start_time of 12:15am on 3/5/2011. Look at this:
irb(main):002:0> a = Appointment.find(15)
=> #<Appointment id: 15, start_time: "2011-03-05 05:15:00", created_at: "2011-03-05 03:42:03", updated_at: "2011-03-05 03:42:03", stylist_id: 13, client_id: 8>
irb(main):003:0> a.start_time
=> Sat, 05 Mar 2011 00:15:00 EST -05:00
As you can see, the date got saved wrong. Interestingly, though, Rails compensates for it when the data comes back out.
I assume my app has always behaved this way and I just didn't notice. Recently, though, I wrote a query that pulls the dates out with raw SQL, so I'm getting the wrong time and it's causing problems.
Can anyone shed some light on why this is happening and what I can do to get around the problem?
Rails does this on purpose. Check your time zone settings:
config.active_record.default_timezone
config.time_zone
http://guides.rubyonrails.org/configuring.html#configuring-active-record
Time zone features were introduced in 2.1 and haven't changed much. This article gives a good explanation:
http://mad.ly/2008/04/09/rails-21-time-zone-support-an-overview/
Rails save datetime in database in UTC time(zero offset) and converts the time to the time zone which we intend when it displays it. So, when we parse directly from the database, you will be getting UTC time. If you try to convert it into time like this:
time_string = #raw string obtained from database(like "2011-03-05 05:15:00")
time_object = Time.parse(time_string)
You will get the time with offset according to the timezone of your machine or server. Ruby parses time like that. It will take the time and give the timezone of your machine(which is incorrect here as the timezone is actually UTC as it is from database). I ran into this problem and solved it by adding UTC when I parse time from raw sql strings from database.
time_string << " UTC"
time_object = Time.parse(time_string) # gives time zone as UTC
then if you use it, the result will be correct.

Weird time inconsistencies between production and development

For some reason times are appearing differently in development (my local Mac) and production (Heroku). Take a look: (Just prior to doing this I did a heroku db:pull, so the databases should be identical)
Production (Heroku)
>> Annotation.last.id
=> 2028
>> Annotation.last.created_at
=> Sat, 12 Sep 2009 06:51:33 UTC +00:00
>> Time.zone
=> #<ActiveSupport::TimeZone:0x2b4972a4e2f0 #tzinfo=#<TZInfo::DataTimezone: Etc/UTC>, #utc_offset=0, #name="UTC">
>> Time.now.zone
=> "PDT"
Development (my Macbook Pro)
>> Annotation.last.id
=> 2028
>> Annotation.last.created_at
=> Sat, 12 Sep 2009 09:51:33 UTC +00:00
>> Time.zone
=> #<ActiveSupport::TimeZone:0x23c92c0 #tzinfo=#<TZInfo::DataTimezone: Etc/UTC>, #utc_offset=0, #name="UTC">
>> Time.now.zone
=> "EDT"
Since the created_at times differ by 3 hours, I assume this is related to the 3 hour difference between EDT and PDT, but I'm not sure what's going on.
EDIT: Here's what the raw data looks like:
sqlite> Select created_at from annotations where id = 2028;
2009-09-12T09:51:33-04:00
It looks like this is a problem when moving databases between Heroku and your development machine. Running the SQL query directly gives me this:
Local: {"created_at"=>"2009-10-30 22:34:55.919586"}
Remote: {"created_at"=>"2009-10-31 01:34:55.919586"}
Same problem, exactly a three hour shift. Looking through the source for Taps, the Gem heroku uses for DB sync, does not provide any clues into what could be going wrong here. I've opened a heroku support ticket, and I'll update this post with what I find.
UPDATE:
Ricardo and Morten from Heroku replied. Specify the TZ on the command line like so:
TZ=America/Los_Angeles heroku db:pull
Looks like it assumes the dates in the DB are stored in your local time-zone which is different for the 2 environments and then it translates it to UTC. since the value in DB is essentially the same you get 2 different UTC values.
What is the value of config.time_zone from your "config/environment.rb"?
Also what is the value of "Select created_at from annotations where id= 2028" ?
This is a problem I've encountered before as well.
If you do something like Annotation.last.created_at on the rails console, the result already has the timezone applied to it. You should look at the "pure" date in mysql, via the mysql console :-)
Perhaps one or both of the machines has it's system time setting set to local timezone, instead of UTC. If that's the case, then it would be confused on what value the UTC time (int) actually is.
There are a number of areas to check on both machines:
system timezone
local(user) process timezone
database system timezone
database local timezone
This doesn't seem explicable by one or the other reporting its local time as the UTC time: Eastern time is 4 hours away from UTC, and Pacific time is 7 hours away, but you're seeing a three-hour difference, not a 4- or 7-hour difference.
It seems like both of them are producing incorrect output. SQLite is clearly saying that the 09:51 time is supposed to be an -04:00 time, i.e. Eastern time, but Rails is incorrectly claiming that it's UTC in one case — and in the other case, it's translating it from Eastern to Pacific, then incorrectly claiming that the result is UTC.
Maybe this is a bug in the SQLite backend for ActiveRecord? Because it seems like the kind of thing that would have been caught and squished long ago if it were in widely-used code.

Resources