Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails ActiveSupport time and timezone error on Alpine Linux

I don't know if I am doing something stupid, so bear with me.

tl;dr Rails ActiveSupport time and timezone seems to have an error on Alpine Linux. It uses the DST variant (summer time) of my timezone when it should use the winter time.

Steps to reproduce:

  1. Start a shell in a Docker Alpine Linux Ruby image:
$ docker run -it --rm ruby:2.7.1-alpine sh

All following steps happen inside the running docker container.

  1. Install timezone data:
$ apk add --no-cache --update tzdata
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz
(1/1) Installing tzdata (2020c-r0)
Executing busybox-1.31.1-r9.trigger
OK: 23 MiB in 37 packages
  1. Print standard ruby current time:
$ ruby -e 'puts Time.now.inspect'
2020-10-28 20:34:24.4817918 +0000

Looks good. Time is printed in UTC.

  1. Besides UTC, standard ruby Time class can only handle local time. This is dependent on the local system timezone, which can be configured using operating system configuration mechanisms, or which can simply be passed to Ruby with the TZ env var. Let's try to use my timezone "Europe/Berlin":
$ TZ="Europe/Berlin" ruby -e 'puts Time.now.inspect'
2020-10-28 21:39:22.7037648 +0100

Looks good. Berlin timezone is UTC+01 in winter (standard) or UTC+02 in summer (DST). At the time of this writing, we have winter time, so +0100 is fine.

  1. Now let's move to ActiveSupport:
$ gem install activesupport
Fetching tzinfo-1.2.7.gem
Fetching i18n-1.8.5.gem
Fetching activesupport-6.0.3.4.gem
# #### many more lines of output ####
Successfully installed activesupport-6.0.3.4
6 gems installed
  1. In contrast to standard ruby, ActiveSupport supports all possible timezones, rather than only two. Getting the current time is Time.current, so let's try this:
$ ruby -e 'require "active_support/all"; puts Time.current.inspect'
2020-10-28 20:43:51.1098842 +0000

This looks not different from the output of step 3. The reason for this is that Time.current only behaves differently than Time.now when a timezone is configured.

  1. We can configure a timezone for ActiveSupport with Time.zone=(timezone_identifier) or with Time.use_zone(timezone_identifier) { "inside this block the timezone is used" }. Let's try the first variant:
$ ruby -e 'require "active_support/all"; Time.zone = "UTC"; puts Time.current.inspect'
Wed, 28 Oct 2020 20:50:55 UTC +00:00

We are still on UTC, but the output looks different than before. From this we know that we got a ActiveSupport::TimeWithZone object. This is good.

  1. Now I want the same for my timezone:
$ ruby -e 'require "active_support/all"; Time.zone = "Europe/Berlin"; puts Time.current.inspect'
Wed, 28 Oct 2020 22:52:21 CEST +02:00

On a first glance this looks good, but compare it carefully with the output of step 4. In "Europe/Berlin" timezone we currently have winter time, UTC+01, also called "CET" (central european time). But this time in the output the timestamp is labeled as being "CEST" (central european summer time), which is UTC+02.

And this is wrong.

Where is the error? Is it in Alpine Linux? Is it in standard Ruby? But the output is correct in step 4. Or is the error in ActiveSupport or its connection to the timezone data? Am I doing something wrong?

like image 346
Christoph Grothaus Avatar asked Oct 28 '20 21:10

Christoph Grothaus


1 Answers

Turns out this is an issue affecting the tzinfo gem (versions < 1.2.8 / < 2.0.3) - it's incompatible with timezone-data 2020b (onwards?), and recent versions of Alpine ship with 2020c. The file format changed from 'fat' to 'slim' which isn't compatible with the tzinfo gem yet.

Updated solution (2020-11-09)

Support for "slim" format zoneinfo files has now been added to the tzinfo gem in releases:

  • v1.2.8
  • v2.0.3

Original Ruby solution

You can use the tzinfo-data gem instead of relying on the system tzinfo:

gem install activesupport tzinfo-data
ruby -e 'require "active_support/all"; Time.zone = "Europe/Berlin"; puts TZInfo::DataSource.get; puts Time.current.inspect'

# Ruby DataSource
# Thu, 29 Oct 2020 16:25:08 CET +01:00

Original Alpine solution

You can rebuild the time zone data package in the 'fat' format (adapted from comment):

FROM ruby:2.7.2-alpine

# Install tzdata because we need the zic binary
RUN apk add --no-cache tzdata

# Fix incompatibility with slim tzdata from 2020b onwards
RUN wget https://data.iana.org/time-zones/tzdb/tzdata.zi -O /usr/share/zoneinfo/tzdata.zi && \
    /usr/sbin/zic -b fat /usr/share/zoneinfo/tzdata.zi

Then testing the time in Europe/Berlin:

gem install activesupport
ruby -e 'require "active_support/all"; Time.zone = "Europe/Berlin"; puts TZInfo::DataSource.get; puts Time.current.inspect'

# Zoneinfo DataSource: /usr/share/zoneinfo
# Thu, 29 Oct 2020 16:29:19 CET +01:00
like image 62
odlp Avatar answered Oct 08 '22 00:10

odlp