diff --git a/cmd/mesh-controller/acts.go b/cmd/mesh-controller/acts.go index 4a0abe8..c5bae72 100644 --- a/cmd/mesh-controller/acts.go +++ b/cmd/mesh-controller/acts.go @@ -39,6 +39,13 @@ import ( // It costs a resolution per machine. Assignment is a person typing a command, and being told which // machines this just blocked is worth more than the milliseconds. func assign(ctx context.Context, open *stores, node, module string) (string, error) { + // Held while it is recorded, so it cannot land between a converge's preview and its flip and + // be taken without ever having been previewed (novox/hq ADR 0100). + ctx, release, err := holdNodes(ctx, open, []string{node}) + if err != nil { + return "", err + } + defer release() if err := open.inventory.Assign(ctx, node, module); err != nil { return "", err } @@ -71,6 +78,11 @@ func assign(ctx context.Context, open *stores, node, module string) (string, err // module off one machine is the ordinary way to stop providing something to another, and nothing // about the command's own output would ever have said so. func unassign(ctx context.Context, open *stores, node, module string) (string, error) { + ctx, release, err := holdNodes(ctx, open, []string{node}) + if err != nil { + return "", err + } + defer release() if err := open.inventory.Unassign(ctx, node, module); err != nil { return "", err } diff --git a/cmd/mesh-controller/adopting_test.go b/cmd/mesh-controller/adopting_test.go index ef81c2a..3eddda5 100644 --- a/cmd/mesh-controller/adopting_test.go +++ b/cmd/mesh-controller/adopting_test.go @@ -502,3 +502,33 @@ func TestTheFlipIsRefusedOnAnAccountNamingNothingReachable(t *testing.T) { t.Fatal("a refused flip changed something") } } + +// An assignment cannot land between a preview and the flip that takes every module: assigning +// holds the node, so it waits for whatever is converging it. +func TestAssigningWaitsForWhateverIsConvergingTheNode(t *testing.T) { + open, _ := anAdoptedAnchor(t) + ctx := t.Context() + if _, err := take(ctx, open, "anchor", "hello-web"); err != nil { + t.Fatal(err) + } + reportsHolding(t, open, heldFile) + preview, err := converge(ctx, open, "anchor", false, "", "") + if err != nil { + t.Fatal(err) + } + saved, savedPoll := inventory.HoldWaitFor, inventory.HoldPoll + inventory.HoldWaitFor, inventory.HoldPoll = time.Second, 50*time.Millisecond + defer func() { inventory.HoldWaitFor, inventory.HoldPoll = saved, savedPoll }() + + var whileFlipping error + sendNodes = func(context.Context, *stores, []string) error { + _, whileFlipping = assign(ctx, open, "anchor", "notes") + return nil + } + if _, err := converge(ctx, open, "anchor", true, digestIn(t, preview), ""); err != nil { + t.Fatal(err) + } + if !errors.Is(whileFlipping, inventory.ErrNodeBusy) { + t.Fatalf("an assignment landed while the node was being converged: %v", whileFlipping) + } +}