Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
32 changes: 32 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,38 @@ 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 not always sufficient, so the message names the
state the file has to be in rather than just saying "restart".** 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.

**State the required condition, not the boot-time cause.** The obvious
correction — branch the advice on why the record was synthesised, absent versus
unreadable — is wrong in a third case, and that was the first attempt here. The
file can have been absent at boot and been created, readably, since: the node
keeps its synthesised record either way, 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 has no applicable advice. A plain restart is exactly
right for them. So the message asks for `releases/RELEASES` to be *absent or
readable* before the restart, which covers all three states and is shorter than
the branch it replaced. Do not turn it back into a case analysis of the cause,
and do not collapse it into a bare "restart the system" either.

**And it must not name `releases/RELEASES` unqualified**, because that is the
file the *release* creates, not necessarily the one the handler reads — see
`Castle.Deployment.root_dir/0` and
[#23](https://github.com/ausimian/castle/issues/23). Where `RELDIR` or
`{sasl, releases_dir}` points elsewhere the two are different files, and the
remedy is then genuinely harder rather than merely differently spelled: the
hook creates one at the root that the handler will not read, so "absent" does
not get the operator out either, and the file the handler *does* read has to be
put there by hand. The message says so. When #23 lands and Castle follows those
overrides, this paragraph and that sentence both need revisiting — the
divergence is the thing being described, and it is the thing #23 removes.

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
8 changes: 8 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@

### Fixed

- The refusal for a system running from a synthesised release record now names a
remedy that works. It said to restart, and a restart alone is enough only when
`releases/RELEASES` is absent or readable: the release creates that file when it
is missing, so a file that is present but unreadable is stepped over on every

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 Qualify the RELEASES path in the release note

When RELDIR or {sasl, releases_dir} relocates the handler's records, this release note still says that an absent root-relative releases/RELEASES makes a restart sufficient; the release hook creates that root file, but the handler continues reading the relocated path and synthesises another record. Although the command text now handles this override, the user-facing release note preserves the same incorrect remedy and should identify the configured handler file or scope the claim to default Mix deployments.

AGENTS.md reference: AGENTS.md:L460-L468

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.

Fixed in 615b69d. You are right, and it is the same correction I had just made to the command text and to AGENTS.md while leaving the note an operator actually reads still saying the wrong thing — the third time in this series that the fix landed in one place and not its restatement, which is the argument for the Castle.Deployment.root_dir/0 single-home convention rather than against it.

The note now identifies the file instead of assuming it: releases/RELEASES under the release root unless RELDIR or the sasl releases_dir parameter points elsewhere, and where one of those does, that a restart cannot fix it alone because the release creates the root file the handler will not read. It points at #23 for Castle following the overrides itself.

I also swept both documents for other unqualified uses. The remaining mentions describe mechanism — what the handler reads at startup, what an unpack writes back — rather than prescribing a remedy, and the make_releases/0 entry was already qualified in the commit before this one.

mix precommit green, 112 tests.

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.

Fixed in fdfa7f5. Correct — init/1 reads it with file:consult/1, so malformed terms fail exactly as permissions do, and the hook leaves an existing file alone whatever is in it. "Readable" would have sent an operator with a corrupt RELEASES round the same loop the message exists to end.

The condition is now absent, or consultable, and the message says why permissions are not the whole of it. Sweeping for the same wording turned up the record-check entry higher in the release note, which still described the cause as the file "not being there" and the remedy as a bare restart; both corrected.

This is the fourth narrowing of this one sentence, and each has been a real defect rather than a rewording, so worth naming the pattern: every version stated a condition that was sufficient for the case I had in mind and not necessary in general — restart, then absent-or-present, then readable. "Consultable by :release_handler" is the first one phrased as what the handler actually requires rather than as what I expected to have gone wrong.

mix precommit green, 112 tests.

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 on the mechanism again — init/1 accepts only {ok, [Term]}, so two valid terms consult fine and are still rejected. Fixed in 8a83d12, and fixed differently from the previous four, because the series itself was the signal.

Each version named a property of the file, each was necessary and not sufficient, and each admitted a narrower counterexample: restart → present-but-unreadable; absent-or-present → created readably after boot; readable → malformed terms; consultable → two valid terms. There was no reason to expect that to stop, and the underlying error was mine rather than incremental: I kept stating the condition I expected to have gone wrong instead of the condition the handler imposes.

So the message now asks for a file :release_handler accepts, and says explicitly that present, readable and parsing are each necessary and none sufficient. That cannot be narrowed again, because it claims no mechanism — and it is the more useful thing to tell an operator anyway, since the handler is what has to take the file.

AGENTS.md records the whole series and says not to substitute whichever internal criterion is current, that being the move that was wrong five times.

mix precommit green, 112 tests.

start and the system comes back on another synthesised record. An operator
following the old message would have restarted indefinitely. It now asks for the
file to be absent or readable before the restart.

- 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
12 changes: 10 additions & 2 deletions lib/castle/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,16 @@ 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. " <>
"Before restarting, make sure the RELEASES file :release_handler reads is " <>
"either readable or absent, because the release creates that file only when 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 Require RELEASES to be consultable before restarting

When this file is permission-readable but contains malformed Erlang terms, release_handler:init/1 still falls back to the synthesised record because file:consult/1 fails, while the preboot hook leaves the existing file untouched. An operator following the new “readable” condition can therefore restart indefinitely; describe the required state as absent or valid/consultable by :release_handler, and align the release note with that wording.

AGENTS.md reference: AGENTS.md:L440-L446

Useful? React with 👍 / 👎.

"is absent - 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 file is releases/RELEASES under the " <>
"release root unless RELDIR or the sasl releases_dir parameter points " <>
"elsewhere; where one of those does, the release creates a file at the root " <>
"that the handler will not read, so the one it does read has to be put there " <>
"and made readable by hand."}

nil ->
{:error, "#{refusal}: no release is running."}
Expand Down
14 changes: 11 additions & 3 deletions test/castle/commands_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,15 @@ 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 =~ "make sure the RELEASES file :release_handler reads is"

# The remedy names the state the file has to be in, not the reason the
# record was synthesised. A bare "restart" loops forever on a file that is
# present and unreadable, because the hook that creates it is guarded on its
# absence - and a case analysis of the cause, which is what this said first,
# has no advice at all for a file that was absent at boot and has been
# created readably since, where a plain restart is all that is needed.
assert message =~ "Absent, or readable, is what a restart needs."
end

test "asks the release the system is running, and not another one" do
Expand Down Expand Up @@ -136,7 +144,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 +201,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