I can never remember which of the many ways to get the time I should use and I always have to look it up. I always find the same StackOverflow answer that says it is critical that I use the correct one but I never know which one that is. Even after reading the answers.
I always want the time in UTC.
Sometimes, I want to convert a timestamp to the user’s local time to write onto a page.
I am not sure about blanket advice here. Time.now can be fine provided your config is config.time_zone = 'UTC' and don’t need ActiveSupport::TimeWithZone . At Discourse for example everything on the server is UTC and client side is in charge of per user time zones like this control for example: 2020-02-11T23:58:00Z
How much can be done about this at the Rails level? It feels like the current situation is an adaptation to the fact that Ruby doesn’t give us as strong a set of time primitives as we need to correctly handle timezones.
I got error reports around 7.50am and started looking into them. A chunk of the
application which was supposed to calculate a running number per number of records for today kept returning a fixed number despite new records being created. After a few minutes, the running number resumed incrementing.
My timezone is +0800. I don’t need to cater for users in other time zones, so the easiest approach for me to handle my local time zone is to set config.time_zone. However, I do want Postgres to continue storing timestamps in UTC, in case things change in future. This means I need to ensure that my local time is converted into UTC before sending it to the DB. config.time_zone handles this seamlessly for the most part.
Turns out the database scope responsible for this chunk uses Date.today instead of Date.current, which means when called before 8am local time, that query will return yesterday’s count. If I had looked at this issue after 8am, I wouldn’t have seen any abnormal behavior.
But the new Time#new initializer now accepts a timezone parameter so we could possibly monkeypatch Time.now to allow for the per-block scoping Rails allows provided we only work on Ruby 2.6 and up.
Is there any good reason, in a rails server, to use local time? (genuine question - not being sarcastic or trying to make a point).
For every case, I can think of (storing, comparing, calculating, future events) except two (accepting dates from the user and writing them to a page) I want the date/time in UTC.
Can we make it super easy to follow the best practice (dates are always in UTC) by making all the common date and time functions return and accept UTC?
Then make it super easy and obvious to convert to and from the user’s local time in controllers and views.
While I am here, it would be nice if rails_ujs told us the user’s timezone.
If I’m building an application which will only be used in a single local time zone, I shouldn’t need to deal with UTC conversion in every interaction with time.
Front end is usually relatively easy to handle. Model level stuff like querying based on date / time, exporting data to CSV with the right date / time is trickier.
Wouldn’t that be easier with the convention that everything from the client is in local time (except the JSON API, maybe) and everything on the server is in UTC? Of course, all bets are off if you have data from outside rails that is in local time but even exporting to CSV could use a simple date.to_local_time convention if necessary.
A scope like this would then always require Date.current to be passed in from the front end, which would be irritating if the calls are deeply nested in some other model / service logic.
For most Rails apps the advice from Thoughtbot is good starting point. Rails internally uses Time.current for methods like 24.hours.ago and ‘just use .current’ is about as simple as we can make it without patching Time.now and Date.today.
The only real need to deviate from that advice is where you have performance issues and by keeping things in native Time instances and disabling the timezone conversion in Active Record you can save quite a considerable amount of processing time if you’re load large numbers of records. You can either disable the conversion globally like this:
or you can skip timezone conversion for specific attributes like this:
class Post < ApplicationRecord
self.skip_time_zone_conversion_for_attributes = [:created_at, :updated_at]
end
If all you’re doing is outputting timestamps then this is fine. However if you’re wanting to do any kind of date/time math on the server then you’ll need to convert to a Time instance in a local time or an ActiveSupport::TimeWithZone so that DST boundaries are handled correctly.
I would certainly recommend that a server’s default timezone is UTC but Ruby’s Time.now can be switched to a different timezone by setting ENV['TZ'] dynamically. However I’d suspect there may be threading issues with that and potentially performance impacts. Trying to re-implement Active Support’s Date/Time functionality entirely using native Time instances would be a big undertaking.
@samsaffron I’ve done some initial testing with the new timezone support in 2.6+ but didn’t see any significant performance gains to be had. Will repeat the tests in 2.7 to see if anything is better.
If I understand correctly that setting the time zone to UTC will make Date.today & Time.now return the correct time/date in UTC then I think this takes care of all my issues.
This has bothered me for so many years that I don’t know why I didn’t discover this simple solution earlier.
I have many opinions on this topic! Our product deals with time tracking so we think about this a lot.
In my experience, it’s easy to accidentally leak times in the wrong time zone. Sam’s method works well if all your rendering is done in JS, but not if you are rendering times in HAML/erb, or in a CSV export, or in emails triggered by background jobs, etc.
What we do is store a time_zone column on every user, and store all actual times as a timestamp without time zone in postgres. Then use an around_action to set Time.zone on each request for the current user. On background jobs, we wrap jobs in TimeZone.in(@user) {}, and are looking at adding linters to enforce this (currently it relies on people remembering… they don’t).
The biggest challenge we face is when you have two users in different time zones and need to display to user A, a time in the correct time zone for user B. For example, user A lives on the west coast but runs a factory on the east coast, and wants to see what time user B (who works in that factory) started work.
If we rendered the time in user A’s time zone, it would be wrong. If we rendered the time in user B’s time zone it’d be right, but what if we also have to display data about user C (who lives in Central time) in the same response?
So we’ve been moving away from having a per-request time zone, to making all our models be time zone aware. Basically when loading any object that has time columns we now look up the time zone to output its times in, from relevant associations you can set on the object (usually the user, but can also be their home location etc).
We have a similar problem as described by @ghiculescu.
We received information from dataloggers, and each datalogger send us information in their way. What do I mean? Some send us in UTC, some in local time and some in local time without DST. Yay!
So we have a similar approach: timestamps are in postgresql without timezone and each equipment and user has its own timezone declared. And we make use of the timezone block and some helpers we created to convert the timezone every time a user needs to be aware of.
It is important also rt note that in this case, dates should be timezone aware sometimes too. Because if the user is in UTC+3 for eg. and equipment sends information in UTC. If you only separate the date part, and it’s near midnight, each timestamp (although the same) will report a different date.