Tz - time zone support for Elixir (alternative to Tzdata that comes with a lot of bugfixes)

In addition to tz, I provide a small tool tz_extra which allows to obtain, for each time zone, the most recent offsets, the countries observing the time zone, and other useful information. It also allow you to list all the countries (iso 3166 file provided by iana).

The purpose is to build time zone pickers and country pickers; for example my attempt in building a time zone picker:

The data can also be automatically updated (utc/dst offset may change, country names may change, …).

More information in the GitHub repo’s README:
https://github.com/mathieuprog/tz_extra

2 Likes

This has an issue. If two applications are using the tz library and each of them add to their own tree, they can probably race each other. It may be better if those are started under the Tz supervision tree and configured using the app environment. /cc @sasajuric

Correct. But please keep in mind :erlang.get(:elixir_compiler_pid) is private API. We will have a Code.can_await_modules_compilation? in Elixir v1.11. So I recommend adding a TODO to use that instead in the future.

Modules are purged after recompilation:

If modules always have the same name, you don’t need to purge them. The VM will do it automatically the next time you load them. If you want to purge to reduce code size, then I would recommend purging it after some period of time - like a minute - as otherwise you may kill processes currently running the purged code.

4 Likes

This is a good point, but I’m not sure app env would be my first choice :slight_smile:

The race condition issue could easily be tackled by having the interface a-la

Tz.PeriodicalUpdater.set_opts(opts)

which would start (or stop, depending on the options) a locally registered updater in the supervision tree of the dep. Repeated calls to this function would reconfigure the updater with the newest options (or amount to a noop).

However, I personally prefer to have as much of activities of the system in my app’s supervision tree. Periodical tz updating is an activity, and I like to have a control of when will that activity be started, and when will it stopped. For example, if I stop the part of the system which depends on tz, then I want to stop the updater too. Supervision tree allows me to have that.

Therefore, I actually like the proposed interface. The race condition can be solved by registering the process locally. However, if two apps are starting the updater the system will fail to start. If one of these apps is a lib dep (i.e. not an end app), I believe that it’s a sign the lib is trying to be overly smart. The decision about such updating should be left to the user.

If there are two apps in a “proper” umbrella (i.e. two otherwise independently deployed app which we’re running together locally for convenience), then some local configuration setting can fix the problem (e.g. don’t run the updater in app1 in dev/test mix env). In an umbrella disguised as a monolith, the issue boils down to a dev choosing one app where the updater will be running.

1 Like

I am not a big fan of the Tz.PeriodicalUpdater.set_opts(opts) approach, because it introduces its own complexities when it comes to understanding which services will be started and run on boot and how they impact shutdown. Also, if you have a supervisor restarting, the dynamically added process would be lost.

However, registering and failing to start is a fair compromise. If a lib is trying to be too smart, as you said, then maybe the lib needs to export its own processes (or use the app env if necessary). So keeping it simple and leaving it up for someone else to tackle is fine (if it ever arises).

1 Like

Neither am I for the same reasons :slight_smile: I only mentioned it as a convenient (i.e. easy, but not necessarily simple) option. I’d still probably have that over app config, but then in the docs I’d advise users to only ever use set_opts in the application start callback. But yeah, a locally registered process would be my first choice here.

The Erlang docs for code purging mention:

If some processes still linger in the old code, these processes are killed before the code is removed.

[…] a process is only considered to be lingering in the code if it has direct references to the code.

So I guess I can just purge without a waiting time (although not sure what is meant by direct reference in the Erlang world).

Any example libraries exporting a process, that takes the approach you recommend? I have actually not much experience with processes yet so either I need an example lib, or I’ll wait for someone to PR, or I’ll wait to have experience:D

If I understood correctly, the issue is having two apps under the same VM, each adding the worker in their supervisor, and triggering the execution of two recompilations at the same time.

I’m not familiar with what the library is doing, but essentially yeah, the potential issue is that two apps start the updater process, and then somehow these multiple updaters step on each other’s toes.

If in practice that’s not an issue (i.e. multiple updaters may run safe in parallel), then I don’t think you need to do anything. Maybe adding an advice to other lib authors in the docs who might use this lib to avoid being smart, and to leave it to the end user instead to configure the updater.

If that is an issue, the fix could be to include something like name: __MODULE__ when starting the updater process. This will ensure that only one instance of the updater is running in the beam. It should be easily testable with the help of start_supervised. Basically you start the updater process in the test, then try to start it again and assert that the result of the 2nd invocation is {:error, {:already_started, pid}}.

If you’re still unsure on how to approach this, let me know and I can take a look at the code and prep a PR. If you decide to do it yourself, you can make a PR and request a review from me (my GH handle is sasa1977).

Regarding prior art, I can’t think of anything off the top of my head, since IMO many Elixir/Erlang libs make some substandard choices. But I still think that this is a solid approach which can ensure singleton property while leaving enough flexibility to the client with respect to providing input parameters and controlling when/where is the process started.

4 Likes

Using :tz, how would I do the equivalent of Tzdata.list_canonical_zones() (Tzdata)?

@mathieuprog Using :tz, how would I do the equivalent of Tzdata.list_canonical_zones() ?

@slouchpie :tz API is intentionally kept as small as possible to implement Calendar.TimeZoneDatabase's behaviour. Besides that, it only provides a function to get the version of the time zone database.

Utility functions around time zones are provided by TzExtra.

To get the time zone identifiers, see TzExtra.time_zone_identifiers/1.

Another function that might interest you if you are building a time zone picker is:
TzExtra.countries_time_zones/1

Those functions are described in the Readme file.
Any feedback is welcome:)

1 Like

Thank you! That’s perfect.

I refactored tz_extra API. To get the list of time zones you have now two functions:

  • TzExtra.time_zone_identifiers/1
  • TzExtra.civil_time_zone_identifiers/1

The latter will only return the time zones attached to a country. For example, it will not return the “Etc/GMT+6” time zone. Both functions take some options, and note that the previous option names have been changed.

I also added Ecto Changeset validators:

  • validate_civil_time_zone_identifier/3
  • validate_time_zone_identifier/3
  • validate_iso_country_code/3

Example:

import TzExtra.Changeset

def changeset(location, attrs) do
  location
  |> cast(attrs, @required_fields ++ @optional_fields)
  |> validate_required(@required_fields)
  |> validate_iso_country_code(:country_code)
  |> validate_civil_time_zone_identifier(:time_zone)
end
3 Likes

Wow! That’s beautiful!

Added a function to tz_extra allowing you to retrieve the canonical time zone of a time zone:
get_canonical_time_zone_identifier/1

iex> TzExtra.get_canonical_time_zone_identifier("Asia/Phnom_Penh")
"Asia/Bangkok"
iex> TzExtra.get_canonical_time_zone_identifier("Asia/Bangkok")
"Asia/Bangkok"

cc @jsmestad

4 Likes

Awesome!

From version 0.17 it is now possible to:

  • specify a custom HTTP client, you may find an example using :finch in the Readme.
    Thank you for the suggestion @benwilson512! Took only a little over a year :smiley:

  • specify a custom directory for the storage of the IANA time zone data files.

But most importantly, a lot of work has been put into the project to increase the performance. It is now very fast, compared to the early versions and compared to the performance of the Tzdata lib.

4 Likes

Do you have some comparisons for that?

2 Likes

Adding benchmarks is part of the roadmap to v1.0. I’ll update this thread when those will be ready:)

Hello there.

I’m using the tz dependency and I want to add the tz_extra dependency.

My doubts are:

  • Do I have to add {TzExtra.UpdatePeriodically, []} as a child in my supervisor too?
  • Can I use the same custom HTTP client as in tz so I don’t have to add castore and mint dependencies?

Cheers.

You would need to add TzExtra.UpdatePeriodically in your supervisor, but then you do not need to add Tz.UpdatePeriodically. This is mentioned in the docs:

TzExtra.UpdatePeriodically also triggers tz 's time zone recompilation; so you don’t need to add Tz.UpdatePeriodically if you added TzExtra.UpdatePeriodically in your supervisor.

TzExtra would take the http client as defined in tz, so it would take your custom http client. Tz.HTTP.get_http_client!() is called in the UpdatePeriodically module. I should add that in the docs.

2 Likes