ADR 0261: an assignment made while its module's build runs is kept pending

The review of mesh-controller#150 found the controller now makes an
assignment after the person's act, which no record decided. Decide it,
cite it from the glossary and to-be 45, and correct issue 325's account
of what is settled where and what is sent.
This commit is contained in:
jochen
2026-10-08 16:46:40 +02:00
parent 63754ca874
commit 3a09387c8e
4 changed files with 135 additions and 38 deletions
+4 -4
View File
@@ -172,13 +172,13 @@ every other domain has something to run on. **Recorded by** the controller and t
**Purpose.** To decide, send and apply what each node runs, and to say why.
**Recorded by** the controller's `inventory` database. **Upstream of** Provisioning, Connectivity, Data
and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 0252.
and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 0252, 0261.
**Uses:** node, machine, module, seat, setting (Module), manifest (Module).
- **assignment** — a module put on a node, with the settings that node gives it.
- **pending assignment** — an assignment of a module whose build is still running, kept and made when that
build registers the module, or ended with why when it does not
([issue 325](../04-ISSUES/325-assign-said-no-such-module-while-its-build-ran/00-report.md)).
- **pending assignment** — an assignment of a module whose build is still running, kept, and made by the
controller when that build registers the module, or ended with why when it does not
([ADR 0261](../02-DECISIONS/0261-an-assignment-made-while-its-modules-build-runs-is-kept-pending-and-the-controller-makes-it.md)).
- **scope** — where a seat or a claim holds: `node`, `site` or `mesh`. `node` is also the scope's
value in a manifest.
- **capacity** — how many holders a seat takes. A capacity-1 seat is exclusive. A higher-capacity seat
@@ -0,0 +1,90 @@
---
topic: what runs on it
status: accepted
date: 2026-10-08
deciders: jochen
reconstructed: false
---
# 261. An assignment made while its module's build runs is kept pending, and the controller makes it
## Context
Since issue 300's fix, a merge that adds a module asks the build seat for it, and the module is registered
when the build's outcome is taken in, minutes later. On 2026-10-08 `assign` was called in that window and
answered "no module of that name"
([issue 325](../04-ISSUES/325-assign-said-no-such-module-while-its-build-ran/00-report.md)). The controller
kept no record of a build it had asked for until the outcome came back, so it could not tell a build in
flight from a typo.
Until now an assignment was always made at the person's act, and refused there if it could not be made.
Keeping it for later means the controller makes an assignment after the act, without the person present.
That is a new kind of act, and it needs a rule.
## Considered Options
1. **Refuse, and say the build is running.** The person waits and calls `assign` again. Rejected: it brings
back the second act by hand that issue 300 removed, and nothing reminds the person to make it.
2. **Keep the assignment only when asked** (an argument such as `wait`). Rejected: a person who knows to ask
for it already knows the build is running; the one who does not is the one this is for.
3. **Keep the assignment by default while a build is in flight, and have the controller make it when the
build registers the module.** Chosen.
## Decision
1. **The controller keeps every build request it makes**, once the ask is made and never before, with who
asked it (a merge, a plan, a person's `build`, `assign`), the repository, the directory, the branch and
the merge's commit. A request whose ask could not be made, or whose asker stopped waiting, is kept with
why. A request is kept for 30 days.
2. **`assign` of a module the catalogue does not hold says which case it is in:**
- *a build is in flight*: a kept request for the module's directory has no outcome yet (within two
hours of asking), or its build succeeded within the last five minutes and is being registered;
- *known, not built*: there is a kept request, and its build failed, was not registered, gave no outcome,
or could not be asked;
- *unknown*: no module registered and no kept request by that name. The answer claims only that, and
names the closest names.
3. **In the first case the assignment is kept as a pending assignment**, with no argument needed. In the
second, `assign` refuses, and its `build` argument asks for the build from where the last request came
from and keeps the assignment pending on it. If a pending assignment of that module to that machine is
already waiting, it is made to wait for the new build; no build is asked that nothing waits on.
4. **The controller makes a pending assignment by the same act as a person's `assign`**, with the same
checks, when the build registers the module. It first takes the machine's hold and claims the pending
assignment, so a withdrawal cannot land in between. A refusal then ends it as refused. Making it sends
nothing, as a person's `assign` sends nothing.
5. **A pending assignment never waits silently.** It expires with the builder's own words when its build
fails or is not registered, and with a sentence when no outcome comes within two hours. The controller
settles pending assignments on a tick of its own, every minute, and on each build's outcome. A read,
such as `status`, settles nothing.
6. **An assignment that was not made is a condition** (`assignment-not-made`, a warning the operator
resolves). It stays open until it is answered: the module is assigned to that machine later, it is
assigned again and pending on another build, or a person takes it back with `unassign`.
7. **`unassign` withdraws a waiting pending assignment.** It is refused, and says so, while the controller
is making one. Ended pending assignments are deleted after 30 days, and a machine's go with it.
## Consequences
- One kind of assignment is now made by the controller, not at the person's act. The person's intention is
still the cause: nothing is assigned that a person did not ask for.
- `status` lists open pending assignments (and is not well while one is open) and those ended in the last
day; the condition carries an assignment not made until it is answered.
- The controller cannot read the forge. A module directory that no merge announced since this record and no
person asked to build still reads as unknown; the refusal says how to build it.
- A build being registered reads as in flight for five minutes after its outcome. A build recorded and never
registered reads so only after that.
## How it is checked
- mesh-controller `cmd/mesh-controller/pending_test.go`: each case of point 2; the claim under the hold and
the refused withdrawal (point 4, 7); a failed ask that never reads as in flight and the asker kept
(point 1); `build` re-pointing a waiting assignment (point 3); a status read that settles nothing, the
tick that does, and a silent build expiring (point 5); the condition raised, and cleared by `unassign` or
a later assignment (point 6); ended rows deleted after 30 days.
- mesh-controller `internal/inventory/pending_test.go`: a pending assignment goes with its machine.
- `TestReplay325`, R325 in mesh-lab's replays register: the live case of issue 325.
## References
- [Issue 325](../04-ISSUES/325-assign-said-no-such-module-while-its-build-ran/00-report.md); issue 300.
- novox/mesh-controller#150, novox/mesh-lab#68.
- [ADR 0207](0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md): modules assigned together
are judged together, so an act naming a module not registered is refused whole.
@@ -4,6 +4,7 @@ status: in-progress
code: [mesh-controller, mesh-host, mesh-tools, mesh-catalog, mesh-sdk, mesh-lab]
updated: 2026-10-08
decisions:
- 02-DECISIONS/0261-an-assignment-made-while-its-modules-build-runs-is-kept-pending-and-the-controller-makes-it.md
- 02-DECISIONS/0254-a-wait-for-a-persons-new-login-is-not-a-gates-failure-and-each-module-of-a-send-keeps-its-own-pass.md
- 02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md
- 02-DECISIONS/0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md
@@ -177,6 +178,14 @@ controller's suite raises, which fails a kind without words.
change `reopened`.
- **`status`** lists open conditions first, urgent before warning, oldest first, silenced ones marked
with their expiry. The all-well sentence requires none open, silenced included.
**Changed 2026-10-08** ([ADR 0261](../../02-DECISIONS/0261-an-assignment-made-while-its-modules-build-runs-is-kept-pending-and-the-controller-makes-it.md)): `status` also
lists every open pending assignment (one kept while its module's build runs) and every one ended in the
last day, with what ended it. The all-well sentence requires none open. An assignment that was not made
(expired or refused) is the condition `assignment-not-made`, a warning the operator resolves, open until
the module is assigned there, assigned again, or taken back with `unassign`. The controller settles
pending assignments on its own one-minute tick and on each build's outcome; reading `status` settles
nothing.
- **ADR 0224's provider standing** is the first kind: `provider-failing`, raised by the provider's
event, cleared by its recovery, its silence after thirty minutes the row S8 below.
- **Retirement** ([ADR 0230](../../02-DECISIONS/0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md))
@@ -4,7 +4,7 @@ opened: 2026-10-08
located-in: [mesh-controller cmd/mesh-controller (acts.go, the assign act; upgrades.go, the merge's build request), mesh-controller internal/inventory (the build request and pending assignment records)]
fixed-by: mesh-controller#150, mesh-lab#68
replay: R325
amended-design:
amended-design: 03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md
---
# 325. Assign said "no module of that name" while the module's build ran
@@ -40,41 +40,43 @@ nothing told the person to make.
## Fix
**The controller keeps every build request it makes, and `assign` reads them** (mesh-controller#150). A build request is kept when a merge asks for a new module, when a person runs `build`, and when
a plan asks for a tier. It is kept with its repository, directory, branch and the merge's commit. A merge
whose request could not be made is kept too, with the reason. Requests older than 30 days are deleted.
The outcome is the `build` record with the same id.
[ADR 0261](../../02-DECISIONS/0261-an-assignment-made-while-its-modules-build-runs-is-kept-pending-and-the-controller-makes-it.md) decides it; mesh-controller#150 builds
it.
**The controller keeps every build request it makes, once the ask is made, and `assign` reads them.** A
request is kept with who asked it (a merge, a plan, a person's `build`, `assign`), its repository,
directory and branch, and the merge's commit. A request whose ask could not be made, or whose asker stopped
waiting, is kept with why, so it never reads as a build in flight. Requests are kept for 30 days. The
outcome is the `build` record with the same id.
An assignment of a module the catalogue does not hold now says which case it is in:
- **A build is in flight**: "sensors is not registered yet: it is being built from
- **A build is in flight** (no outcome yet, within two hours of asking; or built within the last five
minutes and being registered): "sensors is not registered yet: it is being built from
novox/mesh-catalog@3da80a4b (modules/sensors) since 15:38:17, as build build-…; it can be assigned once
that build registers it". The assignment is kept as a **pending assignment** (new glossary word). When
the build registers the module, the assignment is made, by the same act and with the same checks as by
hand. If the build fails, is not registered, or says nothing within two hours, the pending assignment
expires with the build's own words. `status` lists every pending assignment that is waiting, and every one
that ended in the last day; while one waits, or one expired or was refused, status does not call the mesh
well. `unassign` withdraws a pending assignment.
- **Known, not built**: said with what came of the last request (failed, with the builder's first line;
no outcome; could not be asked), and refused. The answer offers `build`, and `assign` with the new
argument `build: "true"`. That argument asks for the build from where the last request was made and keeps
the assignment pending on it.
- **Unknown**: refused as "no module of that name", as before. The answer adds the closest names the mesh
holds and says how a module in a repository is built.
that build registers it". The assignment is kept as a **pending assignment**, and the controller makes
it, by the same act as a person's `assign`, when the build registers the module. It claims the pending
assignment under the machine's hold first, so an `unassign` cannot land in between; an `unassign` while
it is being made is refused and says so. If the build fails, is not registered, or says nothing within
two hours, the pending assignment expires with the build's own words and raises the condition
`assignment-not-made`. That condition stays open until the module is assigned there, assigned again, or
taken back with `unassign`. The controller settles pending assignments on its own one-minute tick and on
each build's outcome; reading `status` settles nothing. `status` lists open pending assignments and those
ended in the last day, and is not well while one is open.
- **Known, not built**: said with what came of the last request, and refused. The answer offers `build`,
and `assign` with the new argument `build: "true"`, which asks for the build from where the last request
came from and keeps the assignment pending on it. If a pending assignment is already waiting, it is made
to wait for the new build.
- **Unknown**: refused as "no module of that name", as before. The answer says only what was looked at (the
catalogue, and the build requests kept for 30 days), and adds the closest names.
**Pending by default, with no argument to ask for it.** An assignment is what a person meant, and the
controller already keeps it when its machine does not resolve. Nothing is sent until a push, or a walk if
the module's first upgrade announcement finds it assigned. `unassign` undoes it. Asking for a
second act once the build lands would bring back the two acts by hand that issue 300 removed. A build is
never asked for by default: it runs on another machine, and a directory whose last build failed is a fault
to look at before it is something to retry.
Making a pending assignment sends nothing, as a person's `assign` sends nothing; `push` sends it.
Several modules in one act are judged together (ADR 0207). So an act naming a module that is not registered
is refused whole, with each module's case said, and nothing is kept pending.
**Not covered.** The controller cannot read the forge, so a module directory on a trunk that no merge
announced since this fix, and no person asked to build, still reads as unknown. The refusal says how to
build it.
**Not covered.** The controller cannot read the forge, so a module directory that no merge announced since
this fix, and no person asked to build, still reads as unknown. The refusal says how to build it.
## How it is checked
@@ -82,11 +84,7 @@ build it.
before the build's outcome, and then the outcome registers it. The assignment must say the build and be
made when the build lands. On the commit before the fix it fails with "no module of that name: sensors".
It is R325 in mesh-lab's replays register (mesh-lab#68).
- `cmd/mesh-controller/pending_test.go` has one test per case: a build in flight (the answer, the pending
record, status and its JSON, the build applying it), a failed build expiring it and then reading as known
and not built, `build: "true"` asking from the last request's source, an unknown name with the closest
names, a several-module act refused whole, `unassign` withdrawing, and a build that says nothing expiring
at the next look while a module registered meanwhile is assigned. The seat passes `build` and `unassign`
refuses it.
- `cmd/mesh-controller/pending_test.go` and `internal/inventory/pending_test.go` hold the tests ADR 0261
lists under *How it is checked*: one per case, and one per point of its decision.
No design names what `assign` answers for a module it does not hold, so no design is amended.
To-be 45 §2 is amended: what `status` lists, and the new condition.