Skip to content

Commit 1716456

Browse files
authored
Merge pull request #19 from ausimian/issue/10-provisional
feat: boot the provisional version after an emulator restart
2 parents 3f32f70 + 3da647a commit 1716456

16 files changed

Lines changed: 3806 additions & 494 deletions

AGENTS.md

Lines changed: 549 additions & 145 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 88 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -102,18 +102,53 @@ In the post-assembly step:
102102

103103
- A `bin/castle` command is added, providing the commands that manage releases.
104104
The standard `bin/<release>` launcher that Mix generates is left untouched.
105-
- The generated `env.sh` is extended with a hook. On the **first** start of a
105+
- A `bin/start` is added, and it does nothing at all. `release_handler`
106+
composes `$ROOT/bin/start <data file>` and installs it as `heart`'s temporary
107+
reboot command while preparing an emulator restart, and `heart` really does
108+
run it. A Castle release is restarted by its supervisor rather than by
109+
`heart`, so the one correct thing for that script to do is exit 0.
110+
- The generated `env.sh` is extended with a hook, and everything in it runs
111+
only for the commands that start the system. On the **first** start of a
106112
deployment it creates `releases/RELEASES`, which is what lets the system
107113
manage its own releases — a short-lived VM, once, and only while that file
108114
is absent. The release root has to be writable for it to succeed; if it is
109115
not, the start still proceeds, with a warning, and `bin/castle unpack` and
110116
`bin/castle install` will later refuse — each reading the running system's
111117
own release records as it acts — rather than upgrade a system that cannot
112-
record what it is running. Every start after the first does nothing at all.
113-
The
114-
hook is also where the provisional version marker left by a relup that
115-
restarts the emulator will be consumed. Any `env.sh` the project supplies
116-
through `rel/env.sh.eex` is preserved, and runs first.
118+
record what it is running.
119+
120+
Every start also runs OTP's `heart`, deliberately configured to do nothing:
121+
`HEART_NO_KILL`, no `HEART_COMMAND`, a beat timeout at heart's documented
122+
maximum, and the inert `bin/start` above. It is there for one reason —
123+
`release_handler` calls `heart:set_cmd/1` while preparing an emulator
124+
restart, and that raises where no `heart` process exists.
125+
126+
If your deployment already asks for `-heart` — in `rel/vm.args.eex`, in
127+
`ELIXIR_ERL_OPTIONS`, in one of `ERL_AFLAGS`, `ERL_FLAGS` and `ERL_ZFLAGS`,
128+
or in `ERL_OTP<major>_FLAGS` — that is fine, and nothing is added beside it:
129+
two of the flag make `init:get_argument(heart)` answer `{ok, [[], []]}`,
130+
which heart's own startup check has no clause for, so the boot would hang
131+
having printed nothing. The hook settles it by asking `erl` what argument
132+
list it would build, so quoting and escaping in those values are read the way
133+
`erl` reads them, and all six places a flag can come from are covered at
134+
once. It asks on every start, which costs one short-lived `erl` that exits
135+
without booting anything; commands that do not start the system — `eval`,
136+
`rpc`, `remote` — ask nothing.
137+
138+
Those three are **assigned**, and `HEART_COMMAND` is **unset**, rather than
139+
defaulted — so a deployment that already has any of them in its environment
140+
still gets a heart that does nothing. There is no opting out of that while
141+
this hook is in use: your supervisor owning the restart is what the rest of
142+
it depends on. A start that displaces one of your settings says so on
143+
standard error, naming what it displaced, rather than failing the boot over a
144+
configuration conflict or losing the setting silently. A deployment that sets
145+
none of them says nothing at all.
146+
147+
And a start that follows such a restart selects the version that was
148+
installed. See *Upgrades that restart the emulator* below.
149+
150+
Any `env.sh` the project supplies through `rel/env.sh.eex` is preserved, and
151+
runs first.
117152
- The generated _name.rel_ is copied into the `releases` folder as _name-vsn.rel_,
118153
which is where `release_handler` looks for it when unpacking a tarball.
119154
- Any checked `relup` is written into the version path of the release.
@@ -142,7 +177,8 @@ Moving the running system from one version to the next is done through `bin/cast
142177
# Unpack myapp-0.1.1.tar.gz, which you have placed in myapp/releases.
143178
> myapp/bin/castle unpack 0.1.1
144179

145-
# Make 0.1.1 the version that is running now, without restarting the VM.
180+
# Make 0.1.1 the version that is running now. Whether the VM is restarted is a
181+
# property of the relup rather than of this command.
146182
> myapp/bin/castle install 0.1.1
147183

148184
# Make it the version that runs on restart too. With no version given, this
@@ -159,9 +195,38 @@ record OTP made up out of its boot script. Each asks the system itself, as it
159195
acts, rather than trusting an answer given earlier; the refusal names the remedy,
160196
which is a restart.
161197

162-
Version selection on restart needs nothing from `Forecastle`: OTP's
163-
`release_handler` records the committed version in `releases/start_erl.data`,
164-
which is exactly where the standard launcher reads it from.
198+
Version selection on restart needs nothing from `Forecastle` once a version has
199+
been committed: OTP's `release_handler` records the committed version in
200+
`releases/start_erl.data`, which is exactly where the standard launcher reads it
201+
from.
202+
203+
### Upgrades that restart the emulator
204+
205+
`bin/castle install` is the same command whichever kind of transition the relup
206+
describes, and it exits 0 only once the version it installed is the one running —
207+
across a reboot, if there is one. What differs is what has to be in place around
208+
it.
209+
210+
**Your supervisor owns the restart.** `release_handler` calls `init:reboot()`,
211+
the operating system process exits, and nothing inside the release starts it
212+
again: `bin/start` is inert and `HEART_COMMAND` is unset — unset by the hook on
213+
every start, even where the environment supplies one — because two things
214+
starting one service is worse than the problem being solved. Run the release
215+
under systemd, a Docker restart policy, Kubernetes or runit. A release started by
216+
hand from a shell will simply stay down until you start it again.
217+
218+
**Until you commit, a restart takes you back.** `release_handler` writes the
219+
installed version to `releases/new_start_erl.data` and leaves
220+
`releases/start_erl.data` naming the version that is still permanent — only
221+
`bin/castle commit` writes that file. So a provisional release that crashes
222+
before it is committed is followed by an ordinary start of the version you were
223+
on, with nobody intervening. `bin/<release> version` reports that version too,
224+
because what it prints is the version *to be booted*; ask the running system if
225+
you want to know what is running.
226+
227+
**Only the one-stage `restart_emulator` is supported.** `mix forecastle.relup`
228+
never generates the two-stage `restart_new_emulator` and refuses it wherever it
229+
finds one; see below.
165230

166231
## The Appup Compiler
167232

@@ -290,20 +355,13 @@ own is missing. A transition it judged hot and `systools` then could not generat
290355
is a failure, so that the default never quietly ships something other than the
291356
upgrade it decided on.
292357

293-
> **`auto` currently refuses a restart transition.** Castle can install a relup
294-
> that restarts the emulator but cannot yet complete the transition (see below),
295-
> so rather than write an upgrade plan that is known not to install, `auto` exits
296-
> non-zero and names the edge that forced the restart and why. It names it before
297-
> generating anything: an edge classification found to need a restart decides the
298-
> run on its own, so a `systools` error from the transitions that were still going
299-
> to be hot cannot be reported in its place. A `restart_emulator` an appup asked
300-
> for by name is refused just as much - the same transition arrived at another
301-
> way - but only becomes visible once there is a script to look at, so it is
302-
> reported after generation. A relup with both kinds in it therefore names the
303-
> classified edge, and the appup's own restart is reported by the run that follows
304-
> once that edge is gone. This is temporary; when the refusal is lifted the run
305-
> proceeds and both kinds are named in one announcement. `--restart` is the
306-
> deliberate override for anyone who wants the relup anyway.
358+
The announcement names every edge that will restart and why — both the ones
359+
classification chose and any `restart_emulator` an appup asked for by name, in one
360+
message, since they are the same transition arrived at two ways. It also says
361+
what that means for reading the install back: `install_release/1` replies
362+
`{ok, Vsn, Descr}` for such a transition, indistinguishably from a completed hot
363+
upgrade, and the emulator then reboots. `--hot` and `--restart` are the ways to
364+
insist on something else.
307365

308366
**`--hot`** requires a genuine hot upgrade of every transition, and exits
309367
non-zero, having written nothing, if one cannot be: a missing appup entry, an
@@ -341,12 +399,9 @@ its own whenever the ERTS version differs between two releases, so a default
341399
that simply generated a relup would ship the two-stage transition without
342400
anybody having chosen it.
343401

344-
Note that a restart transition can be *generated* but not yet *performed*.
345-
`release_handler` calls `heart:set_cmd/1` while preparing the reboot, which fails
346-
where there is no `heart` process, so the install fails before anything reboots;
347-
and the reboot would come back up on whichever version
348-
`releases/start_erl.data` names, which nothing writes until the release is
349-
committed. Until [castle#14](https://github.com/ausimian/castle/issues/14) and
350-
[#10](https://github.com/ausimian/forecastle/issues/10) land, treat a restart
351-
relup as something to generate and inspect rather than to deploy - and that is
352-
why `auto` refuses to produce one, while `--restart` still will.
402+
Performing a one-stage restart transition takes two things the release now
403+
carries: a `heart` process, because `release_handler` calls `heart:set_cmd/1`
404+
while preparing the reboot, and something to select the installed version on the
405+
way back up, because the reboot would otherwise come back on whichever version
406+
`releases/start_erl.data` names. Both are in the `env.sh` hook; see
407+
*Upgrades that restart the emulator* above for what your supervisor has to do.

0 commit comments

Comments
 (0)