diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 416d1b6..c49c539 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -380,7 +380,20 @@ func (a *Agent) NotComposed(ctx context.Context, why string) { // writeHave writes a module's have; on a node composing its own, with the sealed values of the composition it names // when the caller gave none (Have.Sealed): those of the composition the agent applied or holds at that digest, else // those the module's last have said at that digest. +// +// A writer that names no composition (one written from the agent's running entry before this agent ran anything of the +// module since its start: held by a failed backup, waiting for a yes, not composed) keeps what the module's last have +// said runs, its fingerprint, digest and sealed values, rather than a have naming nothing: what runs is not lost, and +// the boxes the composer reuses after a restart stay (review of L2, rounds 2 and 3). func (a *Agent) writeHave(ctx context.Context, h Have) { + if h.Composed == "" && h.Module != NodeModule { + if last, ok := a.env.Haves.Read(ctx, h.Module); ok && last.Composed != "" { + h.FP, h.Composed = last.FP, last.Composed + if h.Sealed == nil { + h.Sealed = last.Sealed + } + } + } if a.env.SelfComposed && h.Sealed == nil && h.Module != NodeModule && h.Composed != "" { h.Sealed = a.sealedOf(ctx, h.Module, h.FP, h.Composed) } @@ -1100,6 +1113,17 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp from, known := a.runningComposition(module) prior := ran || a.prior[module] a.mu.Unlock() + if !ran && !known { + // Nothing of it run since this agent's start, and no kept declaration: its last have says what runs (review of L2, + // round 3). The same composition is no move; another one is a move whose source cannot be read, so it waits for + // a yes rather than being taken as the module's first composition. + if h, ok := a.env.Haves.Read(ctx, module); ok && h.Composed != "" { + if h.Composed == comp.Dig { + return false + } + prior = true + } + } var j delivery.Judged switch { case known: diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index a2d7a15..44290f5 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -965,7 +965,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( b := newBed(t) b.env.SelfComposed, b.env.Trusted = true, nil b.env.Policy = func(m string) string { - if m == "db" { + if m == "db" || m == "rec" { return catalogue.PolicyRecord } return catalogue.PolicyRoll @@ -1039,7 +1039,8 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( // A record module: its first composition is no move; a new image recreates its container and waits for a yes. container := func(image string) json.RawMessage { - raw, _ := json.Marshal(map[string]any{"id": "db.server", "type": "container", "module": "db", "name": "db", "image": image}) + module := strings.SplitN(image, "@", 2)[0] + raw, _ := json.Marshal(map[string]any{"id": module + ".server", "type": "container", "module": module, "name": module, "image": image}) return raw } dk, draw, _ := unsigned("db", fp, container("db@sha256:"+strings.Repeat("1", 64))) @@ -1048,18 +1049,40 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( eventually(t, "the record module's first composition was not applied", func() bool { return len(b.appliedList()) == 3 }) fpB := fpOf("b") dk2, draw2, ddig2 := unsigned("db", fpB, container("db@sha256:"+strings.Repeat("2", 64))) + // rec is a record module this agent has run nothing of since its start (the switch and every delivery of the + // node-engine restart it): its have alone says what runs. Its move waits for a yes as db's does, and its have keeps + // what runs (fingerprint, digest, sealed values) rather than naming nothing (review of L2, round 3). + recFP, recDig := fpOf("d"), "sha256:"+strings.Repeat("8", 64) + b.haves.Write(context.Background(), Have{Module: "rec", FP: recFP, Composed: recDig, Phase: string(delivery.PhaseRunning), + Sealed: []string{`{"box":"rec"}`}}) + rk, rraw, rdig := unsigned("rec", fpB, container("rec@sha256:"+strings.Repeat("3", 64))) b.comps.put(dk2, draw2) + b.comps.put(rk, rraw) want("db", fpB, "") - eventually(t, "the move did not wait for a yes", func() bool { - h := b.haves.last("db") - return h.Phase == string(delivery.PhaseWaitingYes) && h.Judgement == string(delivery.JudgedRecreates) && h.Waits == ddig2 - }) - if got := b.appliedList(); len(got) != 3 { + want("rec", fpB, "") + applied := len(b.appliedList()) + for _, row := range []struct { + module, fp, composed, waits string + sealed []string + }{{"db", fp, "", ddig2, nil}, {"rec", recFP, recDig, rdig, []string{`{"box":"rec"}`}}} { + eventually(t, row.module+"'s move did not wait for a yes", func() bool { + h := b.haves.last(row.module) + return h.Phase == string(delivery.PhaseWaitingYes) && h.Waits == row.waits + }) + h := b.haves.last(row.module) + if h.FP != row.fp || (row.composed != "" && h.Composed != row.composed) || h.Composed == "" || + (row.sealed != nil && !slices.Equal(h.Sealed, row.sealed)) { + t.Errorf("%s's have waiting for a yes does not keep what runs: %+v", row.module, h) + } + } + if got := b.appliedList(); len(got) != applied { t.Fatalf("a move waiting for a yes was applied: %v", got) } want("db", fpB, ddig2) + want("rec", fpB, rdig) eventually(t, "the yes did not apply the move", func() bool { - h := b.haves.last("db") - return len(b.appliedList()) == 4 && h.Phase == string(delivery.PhaseRunning) && h.FP == fpB + h, r := b.haves.last("db"), b.haves.last("rec") + return len(b.appliedList()) == applied+2 && h.Phase == string(delivery.PhaseRunning) && h.FP == fpB && + r.Phase == string(delivery.PhaseRunning) && r.FP == fpB && r.Composed == rdig }) }