@@ -21,6 +21,50 @@ Castle's job is configuration and release management on a running node.
2121- ** ` unpack/1 ` , ` install/1 ` , ` commit/1 ` , ` remove/1 ` , ` releases/0 ` ** — wrappers
2222 over ` :release_handler ` , with ` generate/1 ` called ahead of ` install ` and
2323 ` commit ` so the target version's configuration exists before it is booted.
24+ - ** ` Castle.running/1 ` ** — succeeds when the version it is given is the release
25+ the system is running. ` install_release/1 ` 's reply says only that the upgrade
26+ was accepted: a transition that restarts the emulator is replied to and then
27+ rebooted, and an emulator upgrade finishes on the way back up, where it can
28+ still roll back. So Castle answers the question and leaves the asking to
29+ Forecastle: ` bin/castle install ` repeats it rather than trusting the reply,
30+ from Forecastle 1.0.0 — the revision pinned in this project's ` mix.lock `
31+ installs with a single rpc and never calls this, so do not describe the
32+ polling as something Castle's own integrated state does. Two conditions. The
33+ version is the running release: the
34+ ` current ` one, or the ` permanent ` one when none is current — ` install ` leaves
35+ its target ` current ` and ` commit ` promotes it, so both count; ` unpacked ` (a
36+ rolled-back continuation) and ` tmp_current ` (written before the reboot) do
37+ not. And its boot has finished, which is ` :init.get_status/0 ` 's * provided*
38+ status being ` :started ` . Do not gate on the internal status: it stays
39+ ` :starting ` for the life of a release started by its boot script, so a booted
40+ node reports ` {:starting, :started} ` . The provided status is what the script's
41+ ` {progress, _} ` instructions move along, and ` started ` is its last one — after
42+ the applications have started, and after ` new_emulator_upgrade/2 ` in the
43+ hybrid script that continues an emulator upgrade. Without that second
44+ condition a poll can confirm a node that is still booting, and automation
45+ that commits straight after installing would make a version that cannot boot
46+ the permanent one.
47+
48+ The marker is the whole of the evidence, so it inherits whatever the selected
49+ boot script does with it. ` RELEASE_BOOT_SCRIPT ` naming a hand-written script
50+ that never reaches ` {progress, started} ` will never be confirmed — ` install `
51+ waits and then fails, and the refusal names the progress the node did reach,
52+ so it is diagnosable and never a false success — and one that emits the marker
53+ before its applications start defeats the check. Both are documented rather
54+ than validated: Mix generates the boot scripts and offers no ` rel/ ` template
55+ for them, so reaching either state takes deliberate work. (An earlier note
56+ here claimed ` systools_make:add_apply_upgrade/2 ` 's hard match on the trailing
57+ marker ruled this out. It does not: that builds the hybrid script for an
58+ emulator upgrade and says nothing about a script an operator supplies.)
59+
60+ Every one of them is a command entry point, so ` Castle ` is the command
61+ boundary: an operation that fails raises ` Castle.Error ` there, which is what
62+ leaves a non-zero exit status behind for the shell that asked for it. Raising,
63+ not halting — the expression runs on the * running* node, so halting would take
64+ down the system under management; ` Kernel.CLI ` catches on the node and
65+ re-raises in the calling VM, and only that VM exits. ` Castle.Commands ` holds
66+ the operations themselves, returning their outcome instead of acting on the
67+ process, which is what makes them testable.
2468
2569Forecastle is what arranges for these to be reachable: it renames ` sys.config `
2670to ` build.config ` at assembly time, adds a ` :preboot ` script that starts
@@ -31,7 +75,10 @@ into this module.
3175
3276| Path | Purpose |
3377| --- | --- |
34- | ` lib/castle.ex ` | The whole of the runtime logic |
78+ | ` lib/castle.ex ` | The command boundary: print the outcome, or raise |
79+ | ` lib/castle/commands.ex ` | The commands themselves, returning their outcome |
80+ | ` lib/castle/error.ex ` | The exception a failed command raises |
81+ | ` test/support/ ` | Stubs for ` :release_handler ` , ` :init ` and a config provider |
3582
3683## Working on this project
3784
@@ -50,27 +97,32 @@ into this module.
5097
5198## Tests
5299
53- There is no test coverage yet. ` test/castle_test.exs ` is a ` doctest ` stub.
100+ ` mix test ` covers ` Castle.Commands ` as units. ` :release_handler ` and ` :init ` are
101+ reached through module arguments that default to them, so the tests hand them
102+ ` Castle.ReleaseHandlerStub ` and ` Castle.InitStub ` instead; ` generate/1 ` takes
103+ the version directory it writes to, so the tests give it a ` tmp_dir ` holding a
104+ synthetic ` build.config ` . ` test/castle_test.exs ` drives the boundary itself
105+ against the real ` :release_handler ` — which is running under ` mix test ` , because
106+ castle depends on sasl — and the real ` :init ` , naming releases that do not
107+ exist.
54108
55- Every function here talks to ` :release_handler ` against a real installed
56- release , and ` generate/1 ` resolves paths from ` :code.root_dir() ` , so none of it
57- is reachable from a plain ` mix test ` . Testing it needs a release fixture booted
58- in a workspace, the way Forecastle's ` :e2e ` suite does — tracked in
59- [ # 8 ] ( https://github.com/ausimian/castle/issues/8 ) . Forecastle's
60- ` test/forecastle/upgrade_test.exs ` exercises this code end to end in the
61- meantime .
109+ What is * not * covered here is a booted release: the upgrade of a running
110+ system , and the exit statuses ` bin/castle ` returns, belong to Forecastle's
111+ ` :e2e ` suite ( [ # 8 ] ( https://github.com/ausimian/castle/issues/8 ) ), which
112+ exercises this code against a real release and asserts on the success messages
113+ each command prints. Those strings — ` Unpacked <vsn> ok ` ,
114+ ` Now running <vsn> (previously <other>). ` , ` Committed <vsn>. … ` and the
115+ ` releases/0 ` table — are a contract with that suite. Failure messages are not .
62116
63117## Known limitations
64118
65- - ** Failed operations exit 0.** Every command catches the ` :release_handler `
66- error, prints it and returns normally, so ` bin/castle ` cannot tell a failed
67- unpack/install/commit/remove from a successful one. Tracked in
68- [ #10 ] ( https://github.com/ausimian/castle/issues/10 ) , together with letting
69- ` generate/1 ` take a caller-chosen destination path
70- ([ #15 ] ( https://github.com/ausimian/castle/issues/15 ) ).
71119- ** Concurrent boots race on ` sys.config ` .** ` generate/1 ` writes into the
72120 version directory, so simultaneous ` start ` /` daemon ` /` eval ` invocations with
73- differing environments overwrite each other's configuration. Same issue.
121+ differing environments overwrite each other's configuration. Do not fix this
122+ by letting callers choose where the configuration is written: it goes away
123+ with [ #13 ] ( https://github.com/ausimian/castle/issues/13 ) , which materialises
124+ the target release's configuration in a ` :peer ` running its own config
125+ providers, and takes ` Castle.generate/1 ` with it.
74126- ** The public API is undocumented.** ` @moduledoc ` is still the generated
75127 placeholder and there are no ` @doc ` or ` @spec ` annotations
76128 ([ #11 ] ( https://github.com/ausimian/castle/issues/11 ) ).
0 commit comments