Skip to content

Commit d22a725

Browse files
authored
Merge pull request #25 from ausimian/issue/13-restart-remedy
fix: name a remedy that works for an unreadable RELEASES
2 parents 21473b0 + 8a83d12 commit d22a725

4 files changed

Lines changed: 99 additions & 11 deletions

File tree

AGENTS.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,55 @@ Castle's job is configuration and release management on a running node.
437437
`RELEASES` file names at least `kernel` and `stdlib`. The remedy the message
438438
names is a restart, because that is the only thing that changes the answer.
439439

440+
**A restart is necessary and not always sufficient, so the message names the
441+
state the file has to be in rather than just saying "restart".** The record is
442+
synthesised when `RELEASES` was missing *or* could not be read, and Forecastle's
443+
`env.sh` creates it only when it is **absent** (`[ ! -f ... ]`). So a file that
444+
is present and unreadable is stepped over on every start: the node comes back on
445+
a freshly synthesised record, the refusal repeats, and an operator following a
446+
message that named only the restart would loop forever.
447+
448+
**State the required condition, not the boot-time cause.** The obvious
449+
correction — branch the advice on why the record was synthesised, absent versus
450+
unreadable — is wrong in a third case, and that was the first attempt here. The
451+
file can have been absent at boot and been created, readably, since: the node
452+
keeps its synthesised record either way, so the refusal still fires, and an
453+
operator told to check whether the file is "absent" or "present and unreadable"
454+
finds it is neither and has no applicable advice. A plain restart is exactly
455+
right for them. So the message asks for `releases/RELEASES` to be *absent or
456+
consultable* before the restart, which covers all three states and is shorter
457+
than the branch it replaced. Do not turn it back into a case analysis of the
458+
cause, and do not collapse it into a bare "restart the system" either.
459+
460+
**Name the authority, not the mechanism — and this is the lesson of five
461+
successive corrections to one sentence.** Each named a property of the file and
462+
each was necessary but not sufficient, so each admitted a narrower
463+
counterexample: "restart" missed a file that was present and unreadable;
464+
"present or absent" missed one created readably since boot; "readable" missed
465+
malformed terms, because `init/1` reads it with `file:consult/1`; "consultable"
466+
missed a file of two valid terms, because `init/1` accepts only `{ok, [Term]}`.
467+
There is no reason to think that series had ended, and the hook leaves an
468+
existing file alone whatever is in it, so every one of those states loops.
469+
470+
The message therefore asks for a file **`:release_handler` accepts**, and says
471+
that no single property of the file is the test. That cannot be narrowed further
472+
because it does not claim a mechanism, and it is what an operator needs anyway:
473+
the handler is the thing that has to take the file. Do not "improve" it by
474+
substituting whichever internal criterion is current — that is the move that was
475+
wrong five times.
476+
477+
**And it must not name `releases/RELEASES` unqualified**, because that is the
478+
file the *release* creates, not necessarily the one the handler reads — see
479+
`Castle.Deployment.root_dir/0` and
480+
[#23](https://github.com/ausimian/castle/issues/23). Where `RELDIR` or
481+
`{sasl, releases_dir}` points elsewhere the two are different files, and the
482+
remedy is then genuinely harder rather than merely differently spelled: the
483+
hook creates one at the root that the handler will not read, so "absent" does
484+
not get the operator out either, and the file the handler *does* read has to be
485+
put there by hand. The message says so. When #23 lands and Castle follows those
486+
overrides, this paragraph and that sentence both need revisiting — the
487+
divergence is the thing being described, and it is the thing #23 removes.
488+
440489
It has to be asked of the node rather than of the filesystem — a file that
441490
appeared *after* the boot that looked for it passes a shell test and still
442491
leaves the node on the synthesised record — and it has to be asked *in the call

RELEASE.md

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,17 @@
4747
- `Castle.unpack/1` and `Castle.install/1` now refuse a system that cannot be
4848
upgraded from, and refuse it in the same call that would otherwise have done
4949
the work. `:release_handler` reads `releases/RELEASES` once, as it starts, and
50-
when the file is not there it makes a release record up out of the boot
51-
script's name and version — a record that names no applications at all.
50+
when it cannot — the file absent, or there but not something it accepts — it
51+
makes a release record up out of the boot script's name and version — a record
52+
that names no applications at all.
5253
Upgrading a system in that state is worse than being stopped: the install
5354
reports success, and every application whose version changed but whose code the
5455
upgrade does not explicitly load goes on running its old code out of the
5556
directory of the release that was just replaced, until a later `remove` deletes
5657
it. Nothing can repair the running system afterwards, because creating the file
57-
changes no record the node holds — so what the refusal says is to restart,
58-
which is the one thing that does: the release creates the file before it
59-
starts.
58+
changes no record the node holds — so what the refusal says is to restart, with
59+
the file either absent or accepted first. See *Fixed* below for what that
60+
condition is and why a bare restart is not always enough.
6061

6162
The question is asked of the node's own records rather than of the filesystem,
6263
which is the only way to see the case where the file exists but the boot that
@@ -70,7 +71,7 @@
7071
operation that *writes* release records: an unpack on such a node would put the
7172
made-up record into `releases/RELEASES`, where the next boot would read it back
7273
as though it belonged there — which takes away the restart that is the way out,
73-
since the file is only created when it is missing. Committing and removing are
74+
since the file is only created when it is absent. Committing and removing are
7475
unaffected: neither can write that record back, and refusing them could strand
7576
a version that was already installed.
7677
- `Castle.upgradable/0`, which answers the same question on its own, for an
@@ -150,6 +151,24 @@
150151

151152
### Fixed
152153

154+
- The refusal for a system running from a synthesised release record now names a
155+
remedy that works. It said to restart, and a restart alone is enough only when
156+
the `RELEASES` file `:release_handler` reads is absent or accepted by the handler
157+
itself: present, readable and parsing as Erlang terms are each necessary and none
158+
of them sufficient. The release creates the file only when it is *absent*, so
159+
anything left in place that the handler will not accept is stepped over on every
160+
start and the system comes back on another synthesised record. An operator following the old message would have restarted
161+
indefinitely.
162+
163+
The refusal now asks for that file to be absent or accepted before the restart,
164+
and identifies it rather than assuming: `releases/RELEASES` under the release
165+
root, unless `RELDIR` or the `sasl` `releases_dir` parameter points elsewhere.
166+
Where one of those does, the two are different files and a restart cannot fix it
167+
on its own — the release creates the one at the root, which the handler will not
168+
read — so the file the handler *does* read has to be put there by hand. Castle
169+
following those overrides itself is
170+
[#23](https://github.com/ausimian/castle/issues/23).
171+
153172
- A release built with `include_erts: false` is now refused, by name and with
154173
the reason, rather than quietly managing the Erlang installation it happens to
155174
be running on. Such a release ships no emulator, so it runs the system one, and

lib/castle/commands.ex

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,8 +344,20 @@ defmodule Castle.Commands do
344344
"not be read, when the system booted. An upgrade from that record reports " <>
345345
"success and leaves any application whose version changed, but whose code the " <>
346346
"upgrade does not load, running its old code. Creating the file now would not " <>
347-
"change the record this node works from. Restart the system: the release " <>
348-
"creates the file before it starts."}
347+
"change the record this node works from, so the system has to be restarted. " <>
348+
"Before restarting, make sure the RELEASES file :release_handler reads is " <>
349+
"either absent or one that :release_handler itself accepts - being present, " <>
350+
"being readable and parsing as Erlang terms are each necessary and none of " <>
351+
"them sufficient, so the test that matters is whether the handler takes it, " <>
352+
"not any one property of the file. The release creates that file only when it " <>
353+
"is absent, so anything left in place that the handler will not accept is " <>
354+
"stepped over on every start and the system comes back on the same " <>
355+
"synthesised record. Absent, or accepted, is what a restart needs. That file " <>
356+
"is " <>
357+
"releases/RELEASES under the release root unless RELDIR or the sasl " <>
358+
"releases_dir parameter points elsewhere; where one of those does, the release " <>
359+
"creates a file at the root that the handler will not read, so the one it does " <>
360+
"read has to be put there by hand."}
349361

350362
nil ->
351363
{:error, "#{refusal}: no release is running."}

test/castle/commands_test.exs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,15 @@ defmodule Castle.CommandsTest do
8585
assert message =~ "This system cannot be upgraded: 1.2.3 is running from a release record"
8686
assert message =~ "names no applications"
8787
assert message =~ "running its old code"
88-
assert message =~ "Restart the system: the release creates the file before it starts."
88+
assert message =~ "make sure the RELEASES file :release_handler reads is"
89+
90+
# The remedy names the state the file has to be in, not the reason the
91+
# record was synthesised. A bare "restart" loops forever on a file that is
92+
# present and unreadable, because the hook that creates it is guarded on its
93+
# absence - and a case analysis of the cause, which is what this said first,
94+
# has no advice at all for a file that was absent at boot and has been
95+
# created readably since, where a plain restart is all that is needed.
96+
assert message =~ "Absent, or accepted, is what a restart needs."
8997
end
9098

9199
test "asks the release the system is running, and not another one" do
@@ -136,7 +144,7 @@ defmodule Castle.CommandsTest do
136144

137145
assert {:error, message} = Commands.unpack("sample-1.2.3", handler)
138146
assert message =~ "Cannot unpack sample-1.2.3: 1.2.2 is running from a release record"
139-
assert message =~ "Restart the system"
147+
assert message =~ "the system has to be restarted"
140148
assert Stub.calls(:unpack_release) == []
141149
end
142150

@@ -193,7 +201,7 @@ defmodule Castle.CommandsTest do
193201
assert {:error, message} = Commands.install("1.2.3", handler)
194202
assert message =~ "Cannot install 1.2.3: 1.2.2 is running from a release record"
195203
assert message =~ "running its old code"
196-
assert message =~ "Restart the system"
204+
assert message =~ "the system has to be restarted"
197205
assert Stub.calls(:install_release) == []
198206
end
199207

0 commit comments

Comments
 (0)