I’m sending a daily report which should be showing the current time set to mountain daylight time. However, after daylight savings time the report has been consistently an hour behind. I if I run Timex.now("MDT")
in the terminal I will get the current time in MDT. Our report is generated much the same way: {:ok, date} = Timex.format(Timex.now("MDT"), "{h12}:{m} {am}")
Again, this works in my terminal but not on the report that gets sent out; it’s an hour behind.
However, if I change Timex.now("EST")
I will get Eastern Time but an hour behind (right now it’s 11:15 EST but I’m getting 10:15).
Any ideas on what the cause of this might be? Is there a Phoenix config somewhere that sets time?
Maybe the production deployment server doesn’t have daylight savings enabled?
Do you have access to it? Can you test with the usual UNIX commands on the terminal?
I can test that for sure, but this happens locally too. If I send the report in a development environment it will be an hour behind.
If I understand correctly, you have a different result for that instruction in your app and in your local terminal?
My approach would be to identify what is different.
I can see potentially two things when calling Timex.now/1
(never used the lib but had a quick look at its code).
- It calls
:os.system_time()
- It relies on
tzdata
lib.
Would you be able to, once from the app and once from your local terminal, check the output of :os.system_time()
and Tzdata.version()
?
Then maybe your local and production DBs don’t have the daylight savings activated? Just shooting in the dark, it’s the first good assumption I could make.
I think I’ve realized the issue. Our code was actually using Timex.now("MST")
I thought MST
and MDT
were interchangeable or that Timex would account for daylight saving time. It seems like MST
will be incorrect about 8 months out of the year, I can’t verify MDT
seems like America/Denver
is the way to go.
@dimitarvp your daylight savings mention got me on this track.
“MST” used as a time zone ID has a constant offset of -7 hours from UTC for the whole year. It’s not a bug. It’s a rule written in the IANA time zone database.
“America/Denver” is a time zone ID and MST/MDT are abbreviations for that time zone.
I didn’t say it’s a bug, when I said “incorrect” I just meant in the case where we had used it to display text on an email. You mention the offset for MST, is MDT then going to be -6 hours from UTC for the whole year as well?
I think I confuse you more:) but the IANA tz database can be very confusing.
“MST” can be used as a time zone ID; but for example “MDT” seems not to be a valid time zone ID.
^ So this should actually throw an error
Anyway, this is not what you want. You want to use “America/Denver” as a time zone ID.
When you use “America/Denver” as a time zone ID, the offset will change throughout the year:
-07:00/MST and -06:00/MDT
It doesn’t throw an error, it just gives the current time (since we are in daylight time). But I am now using “America/Denver” as the time zone ID for much of the app. Thanks @thojanssens1.
This is actually how timezones work. Timezone names alike MDT are names for certain UTC offsets. Area names like “America/Denver” are the names, which define which of those timezones is in effect in a certain area of the world at a certain time.
Perfect
Just to show you what I meant:
iex> Tzdata.zone_exists?("MST")
true
iex> Tzdata.zone_exists?("MDT")
false # <- see here
iex> Timex.now("MDT")
#DateTime<2020-03-26 19:55:03.637000-06:00 MDT MST7MDT>
“MDT” doesn’t exist as a time zone ID, but Timex will find a time zone ID that contains the string “MDT”, and finds MST7MDT, whish does change according to US DST rules.
Ouch… I don’t know why Timex would not return an error instead honestly, that leads to bugs ;-p
If one ever needs a fixed offset, better use a time zone ID such as Etc/GMT-7.
Anyway, it was just to complete the information. You use the right time zone ID now.
I thought this would solve this, but alas, over the weekend the report was sending at 1 as expected but the label in the email is saying 12.
So digging into this on the servers if I run timedatectl
I get:
Local time: Mon 2020-03-30 14:26:50 MDT
Universal time: Mon 2020-03-30 20:26:50 UTC
RTC time: Mon 2020-03-30 20:26:51
Time zone: America/Denver (MDT, -0600)
Just for kicks I started iex on the server and ran Timex.now("America/Denver")
and got #DateTime<2020-03-30 14:29:08.654412-06:00 MDT America/Denver>
. So unless there is another server time setting I’m missing (I also tried date
and hwclock
both revealed my current time).
The email sends locally with the correct time.
@travisf Are your servers not running on UTC time?
I believe they are, isn’t RTC not being in UTC an evidence that they are? I’m not positive but that’s what I’m assuming.
So if Timex gives you the right time on the server for America/Denver (as you proved by opening an iex session on the server and show date with timex), then my next question would be:
- Who sends this report? How is the sending triggered and at what time (and which time zone)?
- Is the report pre-generated or generated at the same time it is sent?
This report is sent by a worker which queues itself to run every 24 hours. There is a config file that starts several workers when the server is restarted with specific times each worker is to run. In the case of this report, 13:00 is the time the report gets sent, this works because we receive the report daily at 1.
The worker calls a function that actually builds the report. Inside that function, we assign an HTML template for the email.
The label in the email is this:
<%= dgettext("report", "Time: %{time}",
time: format_date(Timex.now("America/Denver"), :time_only)
)
%>
format_date
is
@time_format "{h12}:{m} {am}"
def format_date(date, :time_only), do: do_format(date, @time_format)`
Can you show the code that shows how the report is sent at 13:00?
Sure.
We have an ExqOrchestrator that is queueing jobs with Exq. The config file looks a little like this:
config :application, Application.Jobs.ExqOrchestrator,
entry_points: [
...
[
queue: "report",
worker: ReportWorker,
task: "build_and_send_report",
at: {13, 0, 0}
]
]
ReportWorker:
def build_and_send_report(opts \\ []) do
# enqueues job for tomorrow
atrs = %{
task: "build_and_send_report",
args: %{},
worker: @worker,
queue: @queue
}
Jobs.perform_on(atrs, Timex.shift(Timex.now("America/Denver"), days: 1))
# functions to actually send the report
Reporting.send_report(opts)
end
That’s Exq and the worker… Looking into the actual Reporting.send_report/1
the function starts to get a bit complicated because we are running a few database queries gathering information and then processing it but it eventually calls a deliver_email
function which calls a ReportEmail
module:
def make_report(report_vals, opts \\ []) do
new_email()
|> from("email@example.com")
|> put_recipients(opts)
|> subject(dgettext("report", "Daily Report "))
|> put_layout({MailerView, :reporting})
|> render(:daily_report, daily_report: report_vals)
end
The layout in Mailerview contains the Timex.now() stamp that is shown on the email.
How is this :at
option consumed?