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:
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.
This is a good point, but I’m not sure app env would be my first choice
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.
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).
Neither am I for the same reasons 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.
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.
@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:)
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
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
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.
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.