layout: a journal held under a move's fence says how to get out (plan 4 final review)
The cut waits for the journal and the journal waits for the cut's fence. The reason now names the instance move and the two ways out: abort the move, or remove the journal now that the row stands at its destination.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+15
−1
internal/layout/pass.go+5 −1internal/layout/pass_linux_test.go+10 −0
diff --git a/internal/layout/pass.go b/internal/layout/pass.go| index 349a00c8c..0b0fedfce 100644 |
| --- a/internal/layout/pass.go |
| +++ b/internal/layout/pass.go |
| @@ -689,7 +689,11 @@ func (p *Pass) recoverHost(ctx context.Context, host string, byHost map[string]r |
| var holdReason string |
| switch { |
| case fence == fenceHeld && intent != j.Mode: |
| - holdReason = fmt.Sprintf("completed to %s under another operation's fence; journal kept at done until that fence is lifted", j.Mode) |
| + // Only an instance move shares the fence slot, and its cut waits |
| + // for this journal to go while the pass waits for the move's |
| + // fence to lift. Neither goes on alone, so the reason names both |
| + // ways out for whoever reads the agent log. |
| + holdReason = fmt.Sprintf("completed to %s under an instance move's fence; journal kept at done. the move waits for this journal and the journal waits for the move. abort the move, or remove %s now that the row stands at its destination, and the move can finish", j.Mode, provision.LayoutJournalPath(p.DataDir, host)) |
| case intent == IntentUnreadable && j.Mode == ModeFlat: |
| // Releasing a completed revert flat before the intent is known is |
| // the forged-revert window again. |
diff --git a/internal/layout/pass_linux_test.go b/internal/layout/pass_linux_test.go| index 6c94909c9..a82dd1354 100644 |
| --- a/internal/layout/pass_linux_test.go |
| +++ b/internal/layout/pass_linux_test.go |
| @@ -861,6 +861,16 @@ func TestRecoverUnderAForeignFenceKeepsTheJournalWhenNotAtTheIntent(t *testing.T |
| if r := res[host]; r.Outcome != "deferred" || !strings.Contains(r.Reason, "fence") { |
| t.Fatalf("held host: %+v", r) |
| } |
| + // The move waits for the journal and the journal waits for the move, |
| + // so the reason has to tell an operator both ways out. |
| + for _, want := range []string{"instance move", "abort the move", provision.LayoutJournalPath(p.DataDir, host), "stands at its destination"} { |
| + if r := res[host]; !strings.Contains(r.Reason, want) { |
| + t.Errorf("reason %q does not say %q", r.Reason, want) |
| + } |
| + } |
| + if r := res[host]; strings.Contains(r.Reason, "\u2014") { |
| + t.Errorf("reason has an em dash: %q", r.Reason) |
| + } |
| if j, found, err := ReadJournal(p.DataDir, host); err != nil || !found || j.Phase != PhaseDone { |
| t.Fatalf("journal: %+v %v %v", j, found, err) |
| } |