From 3a09387c8ee243252e9a44a561b778b747814b63 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 16:24:01 +0200 Subject: [PATCH] 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. --- 00-META/glossary.md | 8 +- ...ept-pending-and-the-controller-makes-it.md | 90 +++++++++++++++++++ .../45-a-core-that-cannot-fail-silently.md | 9 ++ .../00-report.md | 66 +++++++------- 4 files changed, 135 insertions(+), 38 deletions(-) create mode 100644 02-DECISIONS/0261-an-assignment-made-while-its-modules-build-runs-is-kept-pending-and-the-controller-makes-it.md diff --git a/00-META/glossary.md b/00-META/glossary.md index 1c464740..e177d8f2 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -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 diff --git a/02-DECISIONS/0261-an-assignment-made-while-its-modules-build-runs-is-kept-pending-and-the-controller-makes-it.md b/02-DECISIONS/0261-an-assignment-made-while-its-modules-build-runs-is-kept-pending-and-the-controller-makes-it.md new file mode 100644 index 00000000..bfb4e8de --- /dev/null +++ b/02-DECISIONS/0261-an-assignment-made-while-its-modules-build-runs-is-kept-pending-and-the-controller-makes-it.md @@ -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. diff --git a/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md b/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md index 782ff579..ce6332d9 100644 --- a/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md +++ b/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md @@ -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)) diff --git a/04-ISSUES/325-assign-said-no-such-module-while-its-build-ran/00-report.md b/04-ISSUES/325-assign-said-no-such-module-while-its-build-ran/00-report.md index 65a78eba..b0de6af8 100644 --- a/04-ISSUES/325-assign-said-no-such-module-while-its-build-ran/00-report.md +++ b/04-ISSUES/325-assign-said-no-such-module-while-its-build-ran/00-report.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.