You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: serialise the whole install, not just the marker's arming
arm_restart/4's three steps are one caller's sequence, and the argument
that release_handler serialises install_release/1 anyway does not carry
them: that serialisation is downstream of the entire protocol. Two
callers read the running release, classify the transition and pass the
marker check before either of them publishes anything, so refusing
before clearing buys nothing across processes. The loser reaches
clear_provisional/3 after the winner's install_release/1 has written
new_start_erl.data, deletes the winner's live evidence, and refuses -
the winner's reboot then comes back on the permanent release, install
waits for a version that never becomes the running one, and the operator
is told that nothing has been changed by the process that changed it.
The protocol is not reordered to avoid that, because publishing before
clearing would leave the marker pairable with a stale new_start_erl.data
and boot a version nothing installed, which is worse. What is added is
that there is only ever one caller in it: the running-release read, the
classification, the arming, install_release/1 and the disarming all run
inside :global.trans/3 over [node()]. The region has to reach that far
for a second reason - restart_planned?/3 is a prediction about the
running release, so a concurrent hot install moves the from-version and
do_get_rh_script/4 then selects a different relup entry from the one
that was classified.
global_name_server is a kernel process and runs whether or not
distribution does, and a lock restricted to [node()] talks to the local
one only, so this works on a node with is_alive() == false - which is
the ordinary case here, and the case it was measured on. trans/3
releases the lock in an after and global monitors the holder, so a
caller that dies does not wedge every later install; retries are
infinity, so there is no aborted to mean anything by. It needs no
process of Castle's own, which a supervised lock server would have added
to the managed system's supervision tree for a command that runs a
handful of times in a deployment's life. [node()] rather than
[node() | nodes()] because every caller arrives by rpc on the running
node: a cluster-wide lock would wait on nodes that share nothing with
the deployment and would still not cover a caller in another VM, which
is the boundary and is documented as one.
It waits rather than refusing, and the waiter then meets the marker
check and is told a restart install is pending - the same message as
before, said about a pair that is complete instead of said while taking
half of it away. Only install takes the lock: unpack, commit and remove
arm nothing, and the materialisation Castle.install/1 does first stays
outside it.
The tests drive two real callers with a barrier at which_releases/0,
which is the first thing inside the region and the last before the
arming - so a caller held there is one that has taken the region and
armed nothing, which is the state two of them could previously occupy at
once. The discriminator is that the second caller's lookup never
happens; the end state is deliberately not one, because the interleaving
that destroys evidence needs a caller suspended between the marker check
and the clearing, and those are adjacent. A third test installs one
relup concurrently from two different running versions, where the same
target is hot from one and a restart from the other, so the
classification is shown to belong to the caller that made it.
Refs: ausimian/forecastle#10
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C4oaMvbR1cbxrZBj8qwkqN
0 commit comments