Skip to content

Commit 9e913f6

Browse files
authored
fix: use Calendar.UTCOnlyTimeZoneDatabase instead of project configured tz database (#324)
Fix #317 The Engine node inherits the configuration of the user's project, so if the user configures a different timezone database, like `Tzdata.TimeZoneDatabase`, the engine may crash when trying to use a database that is not really available: ```elixir %ArgumentError{message: "cannot add 10 second to ~U[2026-01-20 07:44:55.937263Z] (with time zone database Tzdata.TimeZoneDatabase), reason: :time_zone_not_found"} ``` The fix here is to use the builtin `Calendar.UTCOnlyTimeZoneDatabase` for the Engine node. I also added a fixture project that replicates the scenario from the linked issue.
1 parent fb269fa commit 9e913f6

File tree

9 files changed

+169
-1
lines changed

9 files changed

+169
-1
lines changed

apps/engine/lib/engine/modules.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ defmodule Engine.Modules do
243243
defp rebuild_cache do
244244
{amount, unit} = @cache_timeout
245245

246-
expires = DateTime.add(DateTime.utc_now(), amount, unit)
246+
expires = DateTime.add(DateTime.utc_now(), amount, unit, Calendar.UTCOnlyTimeZoneDatabase)
247247

248248
module_map =
249249
Map.new(:code.all_available(), fn {module_charlist, _path, already_loaded?} ->
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
defmodule Expert.Engine.ModulesTest do
2+
use ExUnit.Case
3+
4+
alias Expert.EngineApi
5+
alias Expert.EngineNode
6+
alias Expert.EngineSupervisor
7+
alias Forge.Project
8+
9+
import Forge.EngineApi.Messages
10+
import Forge.Test.Fixtures
11+
12+
describe "Engine.Modules with custom time zone database config" do
13+
@tag timeout: :timer.seconds(60)
14+
test "with_prefix/1 works when project configures custom time_zone_database" do
15+
# Regression test for https://github.com/elixir-lang/expert/issues/317
16+
#
17+
# When a project configures a custom time_zone_database (e.g. Tzdata),
18+
# the engine node inherits this config when Mix.Task.run(:loadconfig)
19+
# is called during project compilation (in Engine.Build.Project).
20+
# However, the tzdata application itself is not started on the engine node.
21+
#
22+
# This caused DateTime.add/3 to fail in Engine.Modules.rebuild_cache/0
23+
# because it would use the globally configured Tzdata.TimeZoneDatabase
24+
# which couldn't resolve time zones without the tzdata app running.
25+
#
26+
# The fix is to explicitly use Calendar.UTCOnlyTimeZoneDatabase when
27+
# calling DateTime.add/4 in rebuild_cache/0.
28+
29+
# The :project_config fixture has:
30+
# config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
31+
project = project(:project_config)
32+
33+
# Clean workspace to ensure fresh state
34+
project |> Project.workspace_path() |> File.rm_rf()
35+
36+
{:ok, _} = start_supervised(Forge.NodePortMapper)
37+
{:ok, _} = start_supervised({EngineSupervisor, project})
38+
{:ok, _, _} = EngineNode.start(project)
39+
40+
EngineApi.register_listener(project, self(), [:all])
41+
42+
# Trigger initial compile which runs `mix loadconfig`
43+
EngineApi.schedule_compile(project, true)
44+
assert_receive project_compiled(), :timer.seconds(30)
45+
46+
# Verify the time_zone_database config was loaded on the engine node
47+
tz_db = EngineApi.call(project, Application, :get_env, [:elixir, :time_zone_database])
48+
assert tz_db == Tzdata.TimeZoneDatabase
49+
50+
# Clear the module cache to force rebuild_cache/0 to be called next
51+
EngineApi.call(project, :persistent_term, :erase, [Engine.Modules])
52+
53+
# This triggers Engine.Modules.rebuild_cache/0 which uses DateTime.add
54+
result = EngineApi.call(project, Engine.Modules, :with_prefix, ["Enum"])
55+
56+
assert is_list(result)
57+
assert Enum in result
58+
end
59+
end
60+
end
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
configuration-*.tar
24+
25+
# Temporary files, for example, from tests.
26+
/tmp/
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Configuration
2+
3+
**TODO: Add description**
4+
5+
## Installation
6+
7+
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
8+
by adding `configuration` to your list of dependencies in `mix.exs`:
9+
10+
```elixir
11+
def deps do
12+
[
13+
{:configuration, "~> 0.1.0"}
14+
]
15+
end
16+
```
17+
18+
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
19+
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
20+
be found at <https://hexdocs.pm/configuration>.
21+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Config
2+
3+
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
defmodule ProjectConfig do
2+
@moduledoc """
3+
Documentation for `ProjectConfig`.
4+
"""
5+
6+
@doc """
7+
Hello world.
8+
9+
## Examples
10+
11+
iex> ProjectConfig.hello()
12+
:world
13+
14+
"""
15+
def hello do
16+
:world
17+
end
18+
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
defmodule ProjectConfig.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :project_config,
7+
version: "0.1.0",
8+
elixir: "~> 1.17",
9+
start_permanent: Mix.env() == :prod,
10+
deps: deps()
11+
]
12+
end
13+
14+
def application do
15+
[
16+
extra_applications: [:logger]
17+
]
18+
end
19+
20+
defp deps do
21+
[
22+
{:tzdata, "~> 1.1"}
23+
]
24+
end
25+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
%{
2+
"certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
3+
"hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
4+
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
5+
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
6+
"mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
7+
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
8+
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
9+
"tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"},
10+
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
11+
}

0 commit comments

Comments
 (0)