|
112 | 112 | script that emits the marker before its applications are started defeats the |
113 | 113 | check instead, since the marker is all there is to go on. |
114 | 114 |
|
115 | | - Nothing can build a relup that restarts the emulator until |
116 | | - [forecastle#4](https://github.com/ausimian/forecastle/issues/4), so the |
117 | | - restart transitions this addresses cannot be exercised end to end yet. The |
118 | | - hot-upgrade path is covered by Forecastle's end-to-end suite; the statuses |
119 | | - themselves are covered by unit tests here. |
| 115 | + Both the hot-upgrade path and the emulator-restart path are covered end to end |
| 116 | + by Forecastle's `:e2e` suite, which polls through a real reboot. |
| 117 | +- Upgrades that restart the emulator now work on a release supervised by |
| 118 | + systemd, Docker, Kubernetes or anything else that owns starting the service. |
| 119 | + `Castle.install/1` recognises such a transition from the relup before it asks |
| 120 | + `:release_handler` for anything, and leaves a marker beside the release records |
| 121 | + naming the version being installed. The launcher's `env.sh` fragment, which |
| 122 | + Forecastle 1.0.0 contributes, consumes that marker on the next start and boots |
| 123 | + the version it names. |
| 124 | + |
| 125 | + Two files have to agree for that to happen, and the reason is worth stating: |
| 126 | + `:release_handler` writes `releases/new_start_erl.data` *before* the reboot and |
| 127 | + nothing ever removes it, so a preparation that failed part-way leaves a file |
| 128 | + naming a version that was never installed. Castle's own marker is what says a |
| 129 | + reboot was really asked for; it is written immediately before the install and |
| 130 | + removed on every path where the install failed, and the launcher requires both |
| 131 | + files and requires them to name one version. An install whose marker cannot be |
| 132 | + written - a release root nothing may write to - is refused rather than |
| 133 | + performed, because the alternative is a reboot that silently comes back on the |
| 134 | + version it was upgrading away from. |
| 135 | + |
| 136 | + Agreeing on a version is not on its own enough, so the pair belongs to one |
| 137 | + install *attempt* rather than to a version. Any `new_start_erl.data` left by an |
| 138 | + earlier attempt is cleared before a new marker is armed - otherwise a retry of |
| 139 | + the same version would arm a marker beside a file it did not write, and a |
| 140 | + restart before the retry reached `:release_handler` would boot a version that |
| 141 | + nothing had installed. |
| 142 | + |
| 143 | + Only one install runs on the node at a time, and that is what makes the |
| 144 | + clearing mean anything: two of them could otherwise both decide to arm before |
| 145 | + either had, and the second would clear the `new_start_erl.data` the first one's |
| 146 | + reboot depends on - leaving the first system to come back on the version it was |
| 147 | + upgrading away from, while the second reported that nothing had been changed. |
| 148 | + An install that has to wait waits, and then finds the first one's marker. |
| 149 | + |
| 150 | + What is serialised is `Castle.install/1` itself, and that includes |
| 151 | + materialising the target's configuration. It is worth saying which parts, since |
| 152 | + "the whole operation" was claimed here while the configuration step was still |
| 153 | + outside: materialising ends by renaming a resolved configuration onto the |
| 154 | + target's `sys.config`, so two callers doing it before either reached the lock |
| 155 | + meant the loser's config providers - evaluated in a VM of their own, with |
| 156 | + whatever environment that caller had - could replace the configuration the |
| 157 | + winner's provisional release was about to boot, after which the loser was |
| 158 | + refused for the winner's marker. The install that was refused decided what the |
| 159 | + install that succeeded booted. Configuration providers are not obliged to |
| 160 | + produce the same answer twice, which is the reason `sys.config.pristine` exists |
| 161 | + in the first place. |
| 162 | + |
| 163 | + So the region now runs from the release-record lookup through the |
| 164 | + configuration step to `install_release/1` and the marker being settled, and a |
| 165 | + caller that is going to be told a restart install is pending is told *before* |
| 166 | + it configures anything. Only the ERTS guard is outside, because it reads two |
| 167 | + directories and refuses without touching anything. |
| 168 | + |
| 169 | + `commit` is serialised the same way, and for a reason that is not obvious: it |
| 170 | + configures the version too, so a duplicate install of the version being |
| 171 | + committed could configure it between commit's two steps - the commit would |
| 172 | + succeed, that install would then fail as already installed, and its |
| 173 | + configuration would be what the newly permanent release booted on the next |
| 174 | + restart. A failed caller deciding what a successful one boots. `unpack` and |
| 175 | + `remove` are not serialised: they configure nothing and arm nothing. |
| 176 | + |
| 177 | + Which kind of transition an install is, is decided from the release the system |
| 178 | + is running, and another install completing in between would change that answer - |
| 179 | + which is the other reason the region reaches past the arming. |
| 180 | + |
| 181 | + A restart install while another one is already pending |
| 182 | + is refused rather than allowed to take over its marker, saying so and changing |
| 183 | + nothing; the marker is consumed by the next start of the deployment, so a |
| 184 | + restart clears one left behind by an install that was interrupted. And the |
| 185 | + marker records which attempt armed it, so a failed install removes only its own |
| 186 | + - a start of the deployment consumes the marker whether or not it goes on to |
| 187 | + boot, so the file at that path when an install fails is not necessarily the one |
| 188 | + that install wrote. It is published by linking a file that is already complete |
| 189 | + into place, the way the pristine configuration above is, so no start can read a |
| 190 | + marker that is half written and a race is refused rather than silently won. |
| 191 | + |
| 192 | + The marker is settled on **every** way out of the install, including the ones |
| 193 | + that do not return: an exit, a throw or a raise out of `install_release/1` is |
| 194 | + caught, the marker dealt with, and the failure then let out unchanged. Before, |
| 195 | + only a returned error cleared it - so an exception left the marker armed, and |
| 196 | + where `:release_handler` had already written its own file the pair was complete |
| 197 | + and the next start booted a version whose install had blown up. |
| 198 | + |
| 199 | + And an install that cannot settle its marker now **says so, and says what it |
| 200 | + means**, rather than reporting the original failure alone. A marker Castle |
| 201 | + could not remove, or could not read well enough to tell whether it was still |
| 202 | + its own, is a live instruction to the next start of that system: the failure |
| 203 | + message names the file, says that `new_start_erl.data` may already be beside |
| 204 | + it, says that an ordinary restart will therefore boot the version the install |
| 205 | + did not finish, and asks for the marker to be removed first. Clearing it used |
| 206 | + to be best effort on the argument that a directory the marker cannot be removed |
| 207 | + from is one it could not have been linked into - which holds only if nothing |
| 208 | + changed in between, and `install_release/1` runs in between. |
| 209 | + |
| 210 | + What the install *reports* is different for such a transition, because |
| 211 | + `install_release/1` replies the same `{ok, Vsn, Descr}` for a completed hot |
| 212 | + upgrade and for one that is about to reboot. Rather than say "Now running", it |
| 213 | + says that the version was installed, that the emulator is restarting, and that |
| 214 | + the version stays provisional until it is committed - which is what |
| 215 | + `releases/start_erl.data` still naming the previous version means. `bin/castle |
| 216 | + install` goes on asking the system what it is running across the reboot, and |
| 217 | + exits 0 once the installed version answers. |
| 218 | + |
| 219 | + The rollback that provisional state buys is real and needs nothing: |
| 220 | + `make_permanent/1` is the only thing that writes `releases/start_erl.data`, so |
| 221 | + a provisional release that dies before `Castle.commit/1` is followed by an |
| 222 | + ordinary start of the version that was permanent before. |
| 223 | + |
| 224 | + The two-stage `restart_new_emulator` transition remains unsupported. It reboots |
| 225 | + into a temporary hybrid release whose version directory holds a boot script and |
| 226 | + a configuration and none of the launcher's own files, so there is nothing for a |
| 227 | + launcher to boot; Forecastle refuses to generate one. |
120 | 228 |
|
121 | 229 | ### Changed |
122 | 230 |
|
|
127 | 235 | release built by Mix that is the file OTP writes; a deployment that sets |
128 | 236 | `RELDIR` or the `sasl` `releases_dir` parameter moves the release records |
129 | 237 | elsewhere, and Castle does not yet follow them. |
| 238 | +- `Castle.install/1` accepts four further arguments, all defaulted, naming the |
| 239 | + releases directory and the modules it talks to. `Castle.install("1.2.3")` is |
| 240 | + unchanged and is still what `bin/castle` calls; the arguments exist so that |
| 241 | + concurrent installs can be exercised through the function an operator actually |
| 242 | + invokes, rather than one layer below it. |
130 | 243 | - `unpack/1`, `install/1`, `commit/1`, `remove/1` and |
131 | 244 | `make_releases/0` now fail when the operation fails, instead of printing the |
132 | 245 | reason and returning normally. These are invoked over `bin/castle`, which |
|
0 commit comments