Fix boot convergence for rolling images

ac40f633af5c · AtlantisPleb · · parent 169f8c3cab77

Fix boot convergence for rolling images

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified lib/openagents/forge/boot_converge.ex
  • modified test/openagents/forge/boot_converge_test.exs

Diff

2 files changed, +99 -21

lib/openagents/forge/boot_converge.ex modified +38 -21

@@ -164,7 +164,7 @@ defmodule OpenAgents.Forge.BootConverge do

164 164
        true
165 165
166 166
      {%{sha: sha, details: details}, %{"sha" => sha, "artifact_digest" => nil}} ->
167
        (details || %{})["artifact_digest"] == nil and OpenAgents.BuildInfo.revision() == sha
167
        image_state_matches?(sha, details || %{})
168 168
169 169
      {%{sha: sha, details: details},
170 170
       %{

@@ -193,31 +193,48 @@ defmodule OpenAgents.Forge.BootConverge do

193 193
  end
194 194
195 195
  defp converge_target(repo, target, sha, details, attempts) do
196
    with {:ok, identity} <- target_identity(target, details),
197
         {:ok, bytes, cache_state} <- artifact_bytes(repo, identity),
198
         {:ok, response} <- DeploymentNode.install_artifact(install_request(identity, bytes)),
199
         :ok <- retain_artifacts(repo, target.id, identity.artifact_digest) do
200
      %{
201
        "schema" => "openagents.forge.boot-convergence.v2",
202
        "state" => "converged",
203
        "ready" => true,
204
        "reason" => cache_state,
205
        "sha" => sha,
206
        "artifact_digest" => identity.artifact_digest,
207
        "manifest_digest" => identity.manifest_digest,
208
        "modules" => response["modules"] || identity.modules,
209
        "attempts" => attempts,
210
        "retry_in_ms" => nil
211
      }
196
    if image_target_matches?(sha, details) do
197
      image_ready("image_matches_live", attempts, sha)
212 198
    else
213
      {:image_matches, ^sha} ->
214
        image_ready("image_matches_live", attempts, sha)
199
      with {:ok, identity} <- target_identity(target, details),
200
           {:ok, bytes, cache_state} <- artifact_bytes(repo, identity),
201
           {:ok, response} <- DeploymentNode.install_artifact(install_request(identity, bytes)),
202
           :ok <- retain_artifacts(repo, target.id, identity.artifact_digest) do
203
        %{
204
          "schema" => "openagents.forge.boot-convergence.v2",
205
          "state" => "converged",
206
          "ready" => true,
207
          "reason" => cache_state,
208
          "sha" => sha,
209
          "artifact_digest" => identity.artifact_digest,
210
          "manifest_digest" => identity.manifest_digest,
211
          "modules" => response["modules"] || identity.modules,
212
          "attempts" => attempts,
213
          "retry_in_ms" => nil
214
        }
215
      else
216
        {:image_matches, ^sha} ->
217
          image_ready("image_matches_live", attempts, sha)
215 218
216
      {:error, reason} ->
217
        degraded(OpenAgents.OperationalLog.code(reason), attempts, reason, sha)
219
        {:error, reason} ->
220
          degraded(OpenAgents.OperationalLog.code(reason), attempts, reason, sha)
221
      end
218 222
    end
219 223
  end
220 224
225
  defp image_state_matches?(sha, details) do
226
    (details["artifact_digest"] == nil and OpenAgents.BuildInfo.revision() == sha) or
227
      image_target_matches?(sha, details)
228
  end
229
230
  defp image_target_matches?(sha, details) do
231
    runtime_digest = OpenAgents.BuildInfo.image_digest()
232
233
    is_binary(runtime_digest) and
234
      OpenAgents.BuildInfo.revision() == sha and
235
      details["image_digest"] == runtime_digest
236
  end
237
221 238
  defp target_identity(target, details) do
222 239
    case details do
223 240
      %{
test/openagents/forge/boot_converge_test.exs modified +61

@@ -348,6 +348,67 @@ defmodule OpenAgents.Forge.BootConvergeTest do

348 348
    assert BootConverge.ready?(@repo)
349 349
  end
350 350
351
  test "image-matching rolling target remains ready despite a non-direct artifact" do
352
    runtime_sha = OpenAgents.BuildInfo.revision()
353
    runtime_digest = "sha256:" <> String.duplicate("f", 64)
354
    previous_digest = Application.get_env(:openagents, :image_digest)
355
    Application.put_env(:openagents, :image_digest, runtime_digest)
356
357
    on_exit(fn -> restore_env(:image_digest, previous_digest) end)
358
359
    {module, binary} = scratch_beam(OpenAgents.NotAllowed.BootConvergeRollingImage)
360
    artifact = artifact(module, binary)
361
362
    target =
363
      insert_target!(
364
        "live",
365
        Map.put(artifact.details, "image_digest", runtime_digest)
366
      )
367
368
    target
369
    |> Ecto.Changeset.change(%{sha: runtime_sha})
370
    |> Repo.update!()
371
372
    previous_enabled = Application.get_env(:openagents, :forge_boot_converge_enabled)
373
    Application.put_env(:openagents, :forge_boot_converge_enabled, true)
374
375
    on_exit(fn -> restore_env(:forge_boot_converge_enabled, previous_enabled) end)
376
377
    assert %{
378
             "state" => "image",
379
             "ready" => true,
380
             "reason" => "image_matches_live",
381
             "sha" => ^runtime_sha
382
           } = BootConverge.converge(@repo)
383
384
    assert BootConverge.ready?(@repo)
385
    refute Code.ensure_loaded?(module)
386
  end
387
388
  test "rolling target stays degraded when its image digest does not match the runtime" do
389
    runtime_sha = OpenAgents.BuildInfo.revision()
390
    previous_digest = Application.get_env(:openagents, :image_digest)
391
    Application.put_env(:openagents, :image_digest, "sha256:" <> String.duplicate("f", 64))
392
393
    on_exit(fn -> restore_env(:image_digest, previous_digest) end)
394
395
    {module, binary} = scratch_beam(OpenAgents.NotAllowed.BootConvergeWrongImage)
396
    artifact = artifact(module, binary)
397
    File.write!(Path.join(Repos.data_dir(), artifact.details["artifact"]), artifact.built.bytes)
398
399
    target =
400
      insert_target!(
401
        "live",
402
        Map.put(artifact.details, "image_digest", "sha256:" <> String.duplicate("e", 64))
403
      )
404
405
    target
406
    |> Ecto.Changeset.change(%{sha: runtime_sha})
407
    |> Repo.update!()
408
409
    assert %{"state" => "degraded", "ready" => false} = BootConverge.converge(@repo)
410
  end
411
351 412
  test "an unreadable cache entry degrades with a bounded reason" do
352 413
    {module, binary} = scratch_beam(OpenAgents.Scratch.BootConvergeUnreadable)
353 414
    artifact = artifact(module, binary)

This page updates live while a promote is in flight · changelog