Skip to content

Commit 7f86ea3

Browse files
author
José Valim
committed
Allow reboot to be disabled in releases, closes #9552
1 parent a54e901 commit 7f86ea3

File tree

6 files changed

+117
-21
lines changed

6 files changed

+117
-21
lines changed

lib/elixir/lib/config/provider.ex

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,13 @@ defmodule Config.Provider do
105105
@callback load(config, state) :: config
106106

107107
@doc false
108-
defstruct [:providers, :config_path, extra_config: [], prune_after_boot: false]
108+
defstruct [
109+
:providers,
110+
:config_path,
111+
extra_config: [],
112+
prune_after_boot: false,
113+
reboot_after_config: true
114+
]
109115

110116
@doc """
111117
Validates a `t:config_path/0`.
@@ -163,13 +169,40 @@ defmodule Config.Provider do
163169
{:ok, %Config.Provider{} = provider} ->
164170
path = resolve_config_path!(provider.config_path)
165171
validate_no_cyclic_boot!(path)
172+
loaded_applications = :application.loaded_applications()
173+
original_config = read_config!(path)
166174

167-
read_config!(path)
168-
|> Config.__merge__([{app, [{key, booted_key(provider, path)}]} | provider.extra_config])
169-
|> run_providers(provider)
170-
|> write_config!(path)
175+
config =
176+
original_config
177+
|> Config.__merge__(provider.extra_config)
178+
|> run_providers(provider)
171179

172-
restart_fun.()
180+
if provider.reboot_after_config do
181+
config
182+
|> Config.__merge__([{app, [{key, booted_key(provider, path)}]}])
183+
|> write_config!(path)
184+
185+
restart_fun.()
186+
else
187+
for {app, _, _} <- loaded_applications, config[app] != original_config[app] do
188+
abort("""
189+
Cannot configure #{inspect(app)} because :reboot_after_config has been set \
190+
to false and #{inspect(app)} has already been loaded, meaning any further \
191+
configuration won't have an effect.
192+
193+
The configuration for #{inspect(app)} before config providers was:
194+
195+
#{inspect(original_config[app])}
196+
197+
The configuration for #{inspect(app)} after config providers was:
198+
199+
#{inspect(config[app])}
200+
""")
201+
end
202+
203+
_ = Application.put_all_env(config, persistent: true)
204+
:ok
205+
end
173206

174207
{:ok, {:booted, path}} ->
175208
File.rm(path)

lib/elixir/test/elixir/config/provider_test.exs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,18 @@ defmodule Config.ProviderTest do
99
import ExUnit.CaptureIO
1010

1111
@tmp_path tmp_path("config_provider")
12-
@env_var "ELIXIR_CONFIG_PROVIDER_PATH"
12+
@env_var "ELIXIR_CONFIG_PROVIDER_BOOTED"
1313
@config_app :config_app
1414
@sys_config Path.join(@tmp_path, "sys.config")
1515

1616
setup context do
17-
System.put_env(@env_var, @tmp_path)
18-
1917
File.rm_rf(@tmp_path)
2018
File.mkdir_p!(@tmp_path)
21-
File.write!(@sys_config, :io_lib.format("~tw.~n", [context[:sys_config] || []]), [:utf8])
19+
write_sys_config!(context[:sys_config] || [])
2220

2321
on_exit(fn ->
2422
Application.delete_env(@config_app, :config_providers)
2523
System.delete_env(@env_var)
26-
System.delete_env("ELIXIR_CONFIG_PROVIDER_BOOTED")
2724
end)
2825
end
2926

@@ -65,8 +62,15 @@ defmodule Config.ProviderTest do
6562
end
6663

6764
test "resolve!" do
68-
assert Provider.resolve_config_path!("/foo") == "/foo"
69-
assert Provider.resolve_config_path!({:system, @env_var, "/bar"}) == @tmp_path <> "/bar"
65+
env_var = "ELIXIR_CONFIG_PROVIDER_PATH"
66+
67+
try do
68+
System.put_env(env_var, @tmp_path)
69+
assert Provider.resolve_config_path!("/foo") == "/foo"
70+
assert Provider.resolve_config_path!({:system, env_var, "/bar"}) == @tmp_path <> "/bar"
71+
after
72+
System.delete_env(env_var)
73+
end
7074
end
7175
end
7276

@@ -116,6 +120,27 @@ defmodule Config.ProviderTest do
116120
init_and_assert_boot()
117121
end) =~ "Got infinite loop when running Config.Provider"
118122
end
123+
124+
test "returns without rebooting" do
125+
reader = {Config.Reader, fixture_path("configs/kernel.exs")}
126+
init = Config.Provider.init([reader], @sys_config, reboot_after_config: false)
127+
Application.put_env(@config_app, :config_providers, init)
128+
129+
assert capture_abort(fn ->
130+
Provider.boot(@config_app, :config_providers, fn ->
131+
raise "should not be called"
132+
end)
133+
end) =~ "Cannot configure :kernel because :reboot_after_config has been set to false"
134+
135+
# Make sure values before and after match
136+
write_sys_config!(kernel: [elixir_reboot: true])
137+
Application.put_env(@config_app, :config_providers, init)
138+
System.delete_env(@env_var)
139+
140+
Provider.boot(@config_app, :config_providers, fn -> raise "should not be called" end)
141+
assert Application.get_env(:kernel, :elixir_reboot) == true
142+
assert Application.get_env(:elixir_reboot, :key) == :value
143+
end
119144
end
120145

121146
defp init(opts) do
@@ -145,4 +170,8 @@ defmodule Config.ProviderTest do
145170
assert_raise ErlangError, fun
146171
end)
147172
end
173+
174+
defp write_sys_config!(data) do
175+
File.write!(@sys_config, :io_lib.format("~tw.~n", [data]), [:utf8])
176+
end
148177
end
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Config
2+
config :kernel, :elixir_reboot, true
3+
config :elixir_reboot, :key, :value

lib/mix/lib/mix/release.ex

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ defmodule Mix.Release do
381381
382382
It uses the following release options to customize its behaviour:
383383
384+
* `:reboot_system_after_config`
384385
* `:start_distribution_during_config`
385386
* `:prune_runtime_sys_config_after_boot`
386387
@@ -389,10 +390,12 @@ defmodule Mix.Release do
389390
@spec make_sys_config(t, keyword(), Config.Provider.config_path()) ::
390391
:ok | {:error, String.t()}
391392
def make_sys_config(release, sys_config, config_provider_path) do
392-
{sys_config, runtime?} = merge_provider_config(release, sys_config, config_provider_path)
393+
{sys_config, runtime_config?} =
394+
merge_provider_config(release, sys_config, config_provider_path)
395+
393396
path = Path.join(release.version_path, "sys.config")
394397

395-
args = [runtime?, sys_config]
398+
args = [runtime_config?, sys_config]
396399
format = "%% coding: utf-8~n%% RUNTIME_CONFIG=~s~n~tw.~n"
397400
File.mkdir_p!(Path.dirname(path))
398401
File.write!(path, :io_lib.format(format, args), [:utf8])
@@ -412,18 +415,27 @@ defmodule Mix.Release do
412415
defp merge_provider_config(%{config_providers: []}, sys_config, _), do: {sys_config, false}
413416

414417
defp merge_provider_config(release, sys_config, config_path) do
415-
{extra_config, initial_config} = start_distribution(release)
418+
{reboot?, extra_config, initial_config} = start_distribution(release)
416419
prune_after_boot = Keyword.get(release.options, :prune_runtime_sys_config_after_boot, false)
417-
opts = [extra_config: initial_config, prune_after_boot: prune_after_boot]
420+
421+
opts = [
422+
extra_config: initial_config,
423+
prune_after_boot: prune_after_boot,
424+
reboot_after_config: reboot?
425+
]
426+
418427
init = Config.Provider.init(release.config_providers, config_path, opts)
419428
{Config.Reader.merge(sys_config, [elixir: [config_providers: init]] ++ extra_config), true}
420429
end
421430

422431
defp start_distribution(%{options: opts}) do
423-
if Keyword.get(opts, :start_distribution_during_config, false) do
424-
{[], []}
432+
reboot? = Keyword.get(opts, :reboot_system_after_config, true)
433+
early_distribution? = Keyword.get(opts, :start_distribution_during_config, false)
434+
435+
if not reboot? or early_distribution? do
436+
{reboot?, [], []}
425437
else
426-
{[kernel: [start_distribution: false]], [kernel: [start_distribution: true]]}
438+
{true, [kernel: [start_distribution: false]], [kernel: [start_distribution: true]]}
427439
end
428440
end
429441

lib/mix/lib/mix/tasks/release.ex

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,13 +609,20 @@ defmodule Mix.Tasks.Release do
609609
are evaluated. You can set it to `true` if you need distribution during
610610
configuration. Defaults to `false`.
611611
612+
* `:reboot_system_after_config` - every time your release is configured,
613+
the system is rebooted to allow the new configuration to take place.
614+
You can set this option to `false` to disable the rebooting for applications
615+
that are sensitive to boot time but, in doing so, note you won't be able
616+
to configure system applications, such as `:kernel`, `:stdlib` and `:elixir`
617+
itself. Defaults to `true`.
618+
612619
* `:prune_runtime_sys_config_after_boot` - every time your system boots,
613620
the release will write a config file to your tmp directory. These
614621
configuration files are generally small. But if you are concerned with
615622
disk space or if you have other restrictions, you can ask the system to
616623
remove said config files after boot. The downside is that you will no
617624
longer be able to restart the system internally (neither via
618-
`System.restart/0` nor `bin/RELEASE_NAME start`). If you need a restart,
625+
`System.restart/0` nor `bin/RELEASE_NAME restart`). If you need a restart,
619626
you will have to terminate the Operating System process and start a new
620627
one. Defaults to `false`.
621628

lib/mix/test/mix/release_test.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,11 +506,23 @@ defmodule Mix.ReleaseTest do
506506
assert File.read!(@sys_config) =~ "%% RUNTIME_CONFIG=true"
507507
{:ok, [config]} = :file.consult(@sys_config)
508508
assert %Config.Provider{} = provider = config[:elixir][:config_providers]
509+
assert provider.reboot_after_config
509510
assert provider.prune_after_boot
510511
assert provider.extra_config == []
511512
assert config[:kernel] == [key: :value]
512513
end
513514

515+
test "writes the given sys_config without reboot" do
516+
release = release(config_providers: @providers, reboot_system_after_config: false)
517+
assert make_sys_config(release, [kernel: [key: :value]], "/foo/bar/bat") == :ok
518+
assert File.read!(@sys_config) =~ "%% RUNTIME_CONFIG=true"
519+
{:ok, [config]} = :file.consult(@sys_config)
520+
assert %Config.Provider{} = provider = config[:elixir][:config_providers]
521+
refute provider.reboot_after_config
522+
assert provider.extra_config == []
523+
assert config[:kernel] == [key: :value]
524+
end
525+
514526
test "errors on bad config" do
515527
assert {:error, "Could not read configuration file." <> _} =
516528
make_sys_config(release([]), [foo: self()], "unused/runtime/path")

0 commit comments

Comments
 (0)