Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,17 @@ Castle's job is configuration and release management on a running node.
`RELEASES` file names at least `kernel` and `stdlib`. The remedy the message
names is a restart, because that is the only thing that changes the answer.

**A restart is necessary and, in one of the two cases, not sufficient — and the
message has to say which.** The record is synthesised when `RELEASES` was
missing *or* could not be read, and Forecastle's `env.sh` creates it only when
it is **absent** (`[ ! -f ... ]`). So a file that is present and unreadable is
stepped over on every start: the node comes back on a freshly synthesised
record, the refusal repeats, and an operator following a message that named
only the restart would loop forever. The message therefore splits the remedy —
restart if the file is absent, make it readable or remove it *first* if it is
present. Do not collapse that back into "restart the system": the diagnosis
already admits both causes, so a single remedy is wrong for one of them.

It has to be asked of the node rather than of the filesystem — a file that
appeared *after* the boot that looked for it passes a shell test and still
leaves the node on the synthesised record — and it has to be asked *in the call
Expand Down
9 changes: 9 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@

### Fixed

- The refusal for a system running from a synthesised release record now names a
remedy that works in both of the cases it diagnoses. It said to restart, and a
restart is enough only when `releases/RELEASES` was *absent*: the release
creates that file when it is missing, so a file that is present but unreadable
is stepped over on every start and the system comes back on another synthesised
record. An operator following the old message would have restarted
indefinitely. It now says to make the file readable or remove it first in that
case.

- A release built with `include_erts: false` is now refused, by name and with
the reason, rather than quietly managing the Erlang installation it happens to
be running on. Such a release ships no emulator, so it runs the system one, and
Expand Down
9 changes: 7 additions & 2 deletions lib/castle/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,13 @@ defmodule Castle.Commands do
"not be read, when the system booted. An upgrade from that record reports " <>
"success and leaves any application whose version changed, but whose code the " <>
"upgrade does not load, running its old code. Creating the file now would not " <>
"change the record this node works from. Restart the system: the release " <>
"creates the file before it starts."}
"change the record this node works from, so the system has to be restarted - " <>
"but check the file first, because which of the two happened decides whether a " <>
"restart is enough. If releases/RELEASES is absent, restart: the release " <>
"creates it before it starts. If it is present and could not be read, make it " <>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cover a RELEASES file created after boot

When releases/RELEASES was absent at boot but is created and becomes readable before this refusal is shown, the added two-way remedy matches neither current condition: the file is no longer absent, and it is not present-and-unreadable. This is a supported state—the node retains its synthesised record even if the file appears later—so telling the operator to infer the boot-time cause by checking the file now can leave them without the applicable remedy, which is simply to restart with the readable file in place. Describe the required pre-restart state rather than branching only on the file's current existence.

AGENTS.md reference: AGENTS.md:L451-L453

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, and the fix is a simplification rather than another branch. Fixed in 5de0e9f.

The third state is real and I had missed it: RELEASES absent at boot, created and readable since. The node keeps the record it synthesised in init/1 regardless, so the refusal still fires — and an operator told to check whether the file is "absent" or "present and unreadable" finds it is neither, and is left without advice at the exact moment a plain restart is all they need.

The error underneath it is the one you name: I was branching on the boot-time cause when what an operator can act on is the state the file has to be in. So the message now asks for one condition instead of analysing two:

Before restarting, make sure releases/RELEASES is either absent or readable: the release creates that file only when it is absent, so an unreadable one left in place is stepped over on every start and the system comes back on the same synthesised record. Absent, or readable, is what a restart needs.

That covers all three states, and it is shorter than the branch it replaces — which is a fair sign the branch was the wrong shape rather than merely incomplete.

AGENTS.md records the dead end explicitly, since "branch the advice on why the record was synthesised" is the obvious correction and is what I reached for first. It now says not to turn it back into a case analysis of the cause, and not to collapse it into a bare "restart the system" either.

The test comment was carrying the same misconception — it described "the other branch of the remedy" — and now states why neither the bare restart nor the case analysis is right.

Worth noting this is the third refusal on this branch series to have claimed more than it could deliver, after the ERTS guard's message was wrong twice the same way. The pattern is consistent enough to be worth naming: a refusal that reports a diagnosis tends to overreach, and one that reports the condition required to proceed does not.

"readable or remove it before restarting - the release only creates that file " <>
"when it is absent, so a restart on its own will step over the unreadable one " <>
"and come back on the same synthesised record."}

nil ->
{:error, "#{refusal}: no release is running."}
Expand Down
12 changes: 9 additions & 3 deletions test/castle/commands_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,13 @@ defmodule Castle.CommandsTest do
assert message =~ "This system cannot be upgraded: 1.2.3 is running from a release record"
assert message =~ "names no applications"
assert message =~ "running its old code"
assert message =~ "Restart the system: the release creates the file before it starts."
assert message =~ "If releases/RELEASES is absent, restart: the release creates it"

# The other branch of the remedy, which a restart alone does not reach: the
# hook that creates the file is guarded on its absence, so an unreadable
# file is stepped over on every start and the node comes back on the same
# synthesised record. A refusal naming only the restart would loop forever.
assert message =~ "make it readable or remove it before restarting"
end

test "asks the release the system is running, and not another one" do
Expand Down Expand Up @@ -136,7 +142,7 @@ defmodule Castle.CommandsTest do

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

Expand Down Expand Up @@ -193,7 +199,7 @@ defmodule Castle.CommandsTest do
assert {:error, message} = Commands.install("1.2.3", handler)
assert message =~ "Cannot install 1.2.3: 1.2.2 is running from a release record"
assert message =~ "running its old code"
assert message =~ "Restart the system"
assert message =~ "the system has to be restarted"
assert Stub.calls(:install_release) == []
end

Expand Down