Summary
Make Castle stop owning config-provider execution. Materialize the target release's runtime configuration as part of install by starting a temporary BEAM that uses the target release's code and normal Elixir Config.Provider machinery.
This issue is the authoritative end-state for runtime configuration handling. Any existing Castle.generate/1 / Forecastle build.config -> sys.config boot hook should be treated as transitional compatibility code to be removed or reduced as this lands.
Motivation
The current Castle.generate/1 reads build.config, invokes stored config providers in the currently running node, and writes releases/<vsn>/sys.config.
That has two architectural problems:
- Installing release B while A is running can execute A's provider modules while trying to configure B.
- Castle/Forecastle duplicates Elixir's own config-provider lifecycle, creating an increasingly large parallel implementation to maintain.
The target architecture is:
normal cold start
-> stock Mix launcher
-> normal Elixir Config.Provider handling
Castle.install(B)
-> start temporary peer using B's code/boot paths
-> run B's normal Config.Provider pipeline
-> persist final B sys.config for release_handler
-> stop peer
-> release_handler.install_release(B)
Proposed direction
Treat target config materialization as an internal step of Castle.install/1:
- Ensure the target release has been unpacked.
- Start a temporary BEAM using
:peer and the target release's boot/code paths.
- Prefer
connection: :standard_io so no EPMD, cookie, or temporary distributed node name is required.
- Let the target release execute its ordinary Elixir config-provider pipeline.
- Persist the final resolved configuration to
releases/<vsn>/sys.config.
- Stop the peer.
- Continue with
:release_handler.install_release/1.
A Castle-owned final provider such as Castle.ConfigWriter may be useful as a sink for the fully resolved config, but Castle should not manually reproduce the provider reduction loop.
Explicit non-goals
- Do not extend the old
Castle.generate/1 design into a more general long-term config engine.
- Do not require Forecastle to keep special-casing
runtime_config_path or arbitrary provider arguments once normal Mix/Elixir handling can own those semantics again.
- Do not introduce another operator command; config materialization belongs inside
install.
Acceptance criteria
- Installing release B while A is running executes B's config-provider code.
releases/B/sys.config exists before B is installed.
- The temporary VM is cleaned up on success and failure.
- The mechanism does not require distributed Erlang.
- Normal cold starts use the stock Mix launcher and normal Elixir runtime configuration semantics.
Castle.generate/1 and Forecastle's bespoke pre-start config-generation path are removed or reduced to transitional/internal code no longer used for the normal boot path.
Scope update (1.0.0 planning)
This is the keystone of the 1.0.0 work, not an incremental improvement.
forecastle#3 removed the forked launcher, but deliberately left the boot-time
configuration machinery in place as a transitional measure: the env.sh fragment
still runs a preboot VM on every start/daemon/eval to expand build.config
into sys.config via Castle.generate/1. That is Castle and Forecastle
reimplementing Elixir's config-provider pipeline. This issue is what licenses its
removal.
Intended end state:
normal cold start Castle install B
│ │
▼ ▼
stock Mix launcher temporary peer using B's code
│ │
▼ ▼
normal Config.Provider normal Config.Provider handling
handling │
▼
materialise B/sys.config
│
▼
release_handler.install_release(B)
Elixir's Config.Provider stays authoritative in both paths. Castle must not
reimplement run_providers/2 — the point of this issue is to stop having a second
implementation, so a solution that hand-rolls the reduction loop in the peer has
missed it. Castle.generate/1 goes away as a public function.
Prefer keeping the mechanism inside Castle rather than adding a
Castle.ConfigWriter provider that Forecastle has to install: the target release
already ships its own preboot boot script (which includes :sasl, :compiler,
:elixir, :castle), so the peer can boot that and call into Castle directly.
Hard dependency: forecastle#6. The peer can only run "the target release's
normal config-provider pipeline" if the target has one, which requires Forecastle
to stop stripping providers and renaming sys.config to build.config. Conversely
forecastle#6 alone breaks install/1, which still reads build.config. The two
changes are atomic and must land together.
Worth a test rather than an assumption: once Forecastle stops stripping providers,
the target's sys.config carries Mix's RUNTIME_CONFIG=true marker, so a later
cold boot of that version re-runs providers over the materialised file. That is
probably correct behaviour, but the hot-install path and the cold-boot path now
write the same file, and the interaction should be verified rather than assumed.
Summary
Make Castle stop owning config-provider execution. Materialize the target release's runtime configuration as part of
installby starting a temporary BEAM that uses the target release's code and normal ElixirConfig.Providermachinery.This issue is the authoritative end-state for runtime configuration handling. Any existing
Castle.generate/1/ Forecastlebuild.config -> sys.configboot hook should be treated as transitional compatibility code to be removed or reduced as this lands.Motivation
The current
Castle.generate/1readsbuild.config, invokes stored config providers in the currently running node, and writesreleases/<vsn>/sys.config.That has two architectural problems:
The target architecture is:
Proposed direction
Treat target config materialization as an internal step of
Castle.install/1::peerand the target release's boot/code paths.connection: :standard_ioso no EPMD, cookie, or temporary distributed node name is required.releases/<vsn>/sys.config.:release_handler.install_release/1.A Castle-owned final provider such as
Castle.ConfigWritermay be useful as a sink for the fully resolved config, but Castle should not manually reproduce the provider reduction loop.Explicit non-goals
Castle.generate/1design into a more general long-term config engine.runtime_config_pathor arbitrary provider arguments once normal Mix/Elixir handling can own those semantics again.install.Acceptance criteria
releases/B/sys.configexists before B is installed.Castle.generate/1and Forecastle's bespoke pre-start config-generation path are removed or reduced to transitional/internal code no longer used for the normal boot path.Scope update (1.0.0 planning)
This is the keystone of the 1.0.0 work, not an incremental improvement.
forecastle#3 removed the forked launcher, but deliberately left the boot-time
configuration machinery in place as a transitional measure: the
env.shfragmentstill runs a preboot VM on every
start/daemon/evalto expandbuild.configinto
sys.configviaCastle.generate/1. That is Castle and Forecastlereimplementing Elixir's config-provider pipeline. This issue is what licenses its
removal.
Intended end state:
Elixir's
Config.Providerstays authoritative in both paths. Castle must notreimplement
run_providers/2— the point of this issue is to stop having a secondimplementation, so a solution that hand-rolls the reduction loop in the peer has
missed it.
Castle.generate/1goes away as a public function.Prefer keeping the mechanism inside Castle rather than adding a
Castle.ConfigWriterprovider that Forecastle has to install: the target releasealready ships its own
prebootboot script (which includes:sasl,:compiler,:elixir,:castle), so the peer can boot that and call into Castle directly.Hard dependency: forecastle#6. The peer can only run "the target release's
normal config-provider pipeline" if the target has one, which requires Forecastle
to stop stripping providers and renaming
sys.configtobuild.config. Converselyforecastle#6 alone breaks
install/1, which still readsbuild.config. The twochanges are atomic and must land together.
Worth a test rather than an assumption: once Forecastle stops stripping providers,
the target's
sys.configcarries Mix'sRUNTIME_CONFIG=truemarker, so a latercold boot of that version re-runs providers over the materialised file. That is
probably correct behaviour, but the hot-install path and the cold-boot path now
write the same file, and the interaction should be verified rather than assumed.