Merge pull request 'ADR 0270 and 0271 (accepted): a held delivery a later merge contains, or that moves nothing, ends by itself; a send held for the bus's step is told at once' (#241) from decision/0270-0271-held-deliveries-end-and-bus-step-said into main
This commit was merged in pull request #241.
This commit is contained in:
+10
-3
@@ -278,7 +278,7 @@ and the operator act. **Recorded by** the controller's `identity` and `licences`
|
||||
**Purpose.** To take one commit from its pull request to every node that should run it, and to put it
|
||||
back when it fails. **Recorded by** the `mesh-delivery` module (deliveries) and the controller (the
|
||||
planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
|
||||
**Decided by** ADR 0162, 0218, 0236, 0237, 0238, 0239, 0267.
|
||||
**Decided by** ADR 0162, 0218, 0236, 0237, 0238, 0239, 0250, 0267, 0270.
|
||||
**Uses:** module, node, declaration and send (Placement), health (Health and repair), person.
|
||||
|
||||
- **merge check** — the two statuses a pull request carries before it may merge: the **merge gate**
|
||||
@@ -313,12 +313,19 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
|
||||
modules live in. `mesh-catalog` and `catalog_modules` are identifiers and keep their spelling.
|
||||
*Not:* ~~catalog~~ (hq)
|
||||
- **delivery** — one commit in one repository on its way to the nodes, from its pull request's head
|
||||
being announced to delivered, failed, superseded or stopped: one pull request, one status, one note
|
||||
being announced to one of its final states: one pull request, one status, one note
|
||||
([ADR 0239](../02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)).
|
||||
Its states are one table (`proposed`, `checked`, `ready` or `rejected`, `published`, `delivering`,
|
||||
`held`, and the final four). Not *change* (a diff). *Pipeline* is the predecessor's build-and-deploy
|
||||
`held`, and the final six: `delivered`, `failed`, `superseded`, `stopped`, `retired` and `covered`;
|
||||
[ADR 0250](../02-DECISIONS/0250-a-merge-heard-only-from-the-buss-history-is-retired-and-owed-work-the-forge-cannot-do-ends.md),
|
||||
[ADR 0270](../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)). Not *change* (a diff). *Pipeline* is the predecessor's build-and-deploy
|
||||
mechanism, which the as-is designs describe; it is never a word for a delivery.
|
||||
*Not:* ~~deployment~~ (hq)
|
||||
- **covered** — the end of a delivery that a later merge containing it delivered: every module it moves was
|
||||
sent to every machine running it by the walk of a later merge into the same trunk, delivered on its own
|
||||
passing check, whose merge commit the forge says contains this one. Not *superseded*, which is a delivery
|
||||
whose walk a newer delivery took over before it ended; nor *delivered*, which its own walk or an empty plan
|
||||
ends in ([ADR 0270](../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)).
|
||||
- **delivery group** — two or more deliveries sharing a pull request head branch name across the mesh's
|
||||
repositories, delivered as one unit in an order declared (`after:`) or inferred by the planner. One
|
||||
level: a group holds deliveries, never groups. Its state is derived from its members, never set.
|
||||
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-09
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
|
||||
---
|
||||
|
||||
# 270. A held delivery that moves nothing, or that a later delivered merge contains, ends by itself, and a hold whose reason is gone lifts
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)
|
||||
decision 2 gives a delivery one table of states. A delivery is `held` when only a person can decide whether
|
||||
it goes on. A person then has four ways out: `release` walks the delivery's own commit, `stop` ends it as
|
||||
stopped, `retire-history` ends history
|
||||
([ADR 0250](0250-a-merge-heard-only-from-the-buss-history-is-retired-and-owed-work-the-forge-cannot-do-ends.md)),
|
||||
and the walk can start on another word (`go`).
|
||||
|
||||
On 2026-10-09, fifteen deliveries were held, most for more than a day, each raising probe D14's
|
||||
`delivery.<id>.stalled` (read with `mesh-delivery.deliveries` and `show`). None of them asked a question a
|
||||
person could answer correctly:
|
||||
|
||||
- **Four merges of hq build nothing.** Their plan says "builds nothing: the change touches no module of the
|
||||
mesh's graph". They were merged before their check answered, and the row *merged unchecked → held* is tried
|
||||
before anything reads the plan.
|
||||
- **Four catalogue merges only add a module.** The controller builds and registers a new module and sends it
|
||||
nowhere, so it opens no walk ([issue 300](../04-ISSUES/300-a-new-module-is-not-built-at-its-merge/00-report.md)).
|
||||
mesh-delivery counted the new module as something a walk moves, so the no-walk rule held each one ten
|
||||
minutes after its merge.
|
||||
- **Five controller merges had no walk of their own.** A later merge into the same trunk was delivered, and
|
||||
its walk sent every module these move. The diagnosis of [issue 319](../04-ISSUES/319-a-held-delivery-a-later-commit-superseded-has-no-end-but-release/01-diagnosis.md)
|
||||
checked each merge commit against the later delivered commit: each was contained in it, and the
|
||||
controller's `status` listed none of their modules as behind.
|
||||
- **Two catalogue merges were held for their group's composed check**
|
||||
([issue 316](../04-ISSUES/316-a-group-member-held-before-its-composed-check-answered-stayed-held-after-it-passed/00-report.md)).
|
||||
One was published while the check was asked and not yet answered. The code read "not answered" as "did
|
||||
not pass" and held it. Two minutes later the check passed for exactly the heads that merged, and nothing
|
||||
read that answer for the held member. The own walk of each of the two was since taken over by a later walk
|
||||
of the same trunk that sent its module again.
|
||||
|
||||
`release` would walk an old commit over a newer one, or walk nothing. `retire-history` refuses each one,
|
||||
because their heads were heard. `stop` is the only way out that works, and it is the wrong record: it sets
|
||||
`mesh/delivery` to `error` on a pull request whose change a later delivered merge carried. Four were stopped that way on
|
||||
2026-10-09 before the issue was found, and their pull requests now say "stopped: covered: …" as an error. A
|
||||
condition that only a wrong act can clear teaches people to ignore the list it is in.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Stop them with why.** Rejected: it is what was done, and it records a delivered change as an error on
|
||||
its pull request. Issue 313 ruled it out for the same reason.
|
||||
2. **A person's act, `supersede`, that ends a held delivery as superseded.** Rejected as the main path. Every
|
||||
fact the person would check can be read by the mesh: the plan, the walks' records and the forge's history.
|
||||
A person asked to confirm a fact the mesh can read is a person asked for nothing. A person keeps `stop` and
|
||||
`release` for what the facts do not settle.
|
||||
3. **Trust merge order alone.** Rejected after review. On a trunk that takes no push, a commit the forge
|
||||
merged later contains every commit merged before it, and mesh-delivery already holds the merge times. But
|
||||
that rests on the trunk's protection, which is the forge's setting and which nothing in mesh-delivery
|
||||
checks. A protection lifted once, or a merge time read wrongly, would end a delivery as covered when its
|
||||
commit is not in the later one. Merge order stays, as the cheap first filter.
|
||||
4. **Ask the forge whether the later merge commit contains this one** — chosen. The forge module gains a
|
||||
read-only tool, `gitea_contains`, over the forge's compare of the two commits: none of this commit's history
|
||||
missing from the later one means it is contained. mesh-delivery is not given the forge's general API tool,
|
||||
which can write. The tool is TypeScript, as the rest of the forge module is; no Go route to the forge's API
|
||||
exists for a module to call. Asking the controller what each machine runs was weighed and left: the
|
||||
controller holds no clones, and what runs is already what the later walk's record says it sent.
|
||||
5. **Read the facts, and end the delivery by observation**, with point 4 deciding containment — below.
|
||||
|
||||
For issue 316 the alternative was to require `mesh/delivery-group` on the trunks, so that no member can merge
|
||||
before its composed check passed. That stays the operator's setting, as ADR 0239 left it. It does not settle
|
||||
a check that answers after the merge.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **A merge that moves nothing is delivered at once.** A delivery whose plan moves no module the mesh holds
|
||||
is delivered as soon as it is merged and has no walk. That covers a plan that builds nothing and a plan
|
||||
that only adds a module the controller builds and sends nowhere. This holds whatever its check said: from
|
||||
`proposed`, `checked` and `rejected`, the row is tried before *merged unchecked → held*, and from `held`
|
||||
it is tried on every tick. `ready → published → delivered` already did this for a merge that passed.
|
||||
**A delivery that only adds a module is reported delivered whatever its build's outcome.** The build is
|
||||
the controller's, asked at the merge, registered when it lands and sent nowhere. A failed build of it
|
||||
raises no condition and is not this delivery's: the controller prints a line in its log, and nothing
|
||||
else says it until a person assigns the module, which is refused while no build of it is registered.
|
||||
This is a known gap, not closed here.
|
||||
2. **A new final state, `covered`: a later merge that contains it was delivered.** A merged delivery is
|
||||
covered when, for every module its plan moves, another delivery:
|
||||
- is of the same repository and was merged into the same trunk, after it by the forge's merge times (the
|
||||
cheap filter);
|
||||
- is `delivered`, and its own check passed;
|
||||
- was delivered by its own walk: the walk's commit is its merge commit, on that trunk;
|
||||
- sent the module to every machine running it, as its machine steps say (sent to all at once, or to the
|
||||
rest after its first machines passed), with no step of it failed or put back;
|
||||
- has a merge commit that contains this one's, as the forge says through `gitea_contains`.
|
||||
|
||||
The forge is asked only once every module has a candidate, once per pair, outside the holder's lock, and its
|
||||
answer is kept on the delivery. A forge that cannot answer means "not yet covered": the delivery stays where
|
||||
it is and is asked about again a minute later. A delivery whose own walk is still open is never covered: its
|
||||
walk delivers it.
|
||||
|
||||
The row is observed. It is tried from `held`, and from `ready` once the ten minutes for a walk to open have
|
||||
passed, before the no-walk hold. From `held` it is tried before `go`. If a walk a person started fails
|
||||
between two readings of a covered delivery, the delivery ends `covered`, not `delivering` then `failed`: the
|
||||
later merge that contains it was delivered whatever its own walk did. The transition's why names each
|
||||
covering delivery, the commit it merged as and its walk. On the pull request, `mesh/delivery` says
|
||||
`covered: a later merge that contains it was delivered: …` as a **success**, never an error. A group counts
|
||||
a covered member as delivered, so the member after it gets its turn and is not stopped.
|
||||
|
||||
`covered` says what was delivered, not what runs now: a revert merged after the covering merge is possible,
|
||||
and is that revert's own delivery.
|
||||
3. **A merge held without a passing check is covered without a person** when a later delivery whose own check
|
||||
passed carries it, and only then. A later delivery's walk runs only on a passing check, so its check has
|
||||
passed in every case this matters for. What a person would have been asked, "does this unchecked change go
|
||||
on?", was answered by the later change, which was checked with this one's change in its history and
|
||||
delivered.
|
||||
4. **A group member is not held for a composed check that has not answered.** "Did not pass for the heads that
|
||||
merged" means the check failed, ran for other heads, or was never asked. A member published while its
|
||||
group's check is asked and not answered waits in `published`, within that state's bound.
|
||||
5. **A hold whose reason is gone lifts.** A member held because its group's composed check had not passed is
|
||||
taken back to `published` when that check passes or warns for the heads that merged, and its own walk is
|
||||
still open. The event is *lifted*, observed, and the member then takes its turn in the group's order. Every
|
||||
other hold stays a person's.
|
||||
6. **Read again on every tick.** The holder reads the facts of every held delivery on each tick, not only when
|
||||
an event arrives. A delivery that was held before these rows existed, or read back after a restart, ends by
|
||||
itself. Once it is final, nothing moves it again.
|
||||
|
||||
The table of ADR 0239 decision 2 gains these rows:
|
||||
|
||||
| From | Event | To | Guard |
|
||||
|---|---|---|---|
|
||||
| proposed, checked, rejected, held | done | delivered | merged, no walk, and its plan moves no module the mesh holds |
|
||||
| ready, held | covered | covered | a later merge that contains it was delivered: merged, no walk of its own open, and every module it moves sent to every machine running it by the walk of a later merge into the same trunk, delivered on its own passing check, whose merge commit the forge says contains this one |
|
||||
| held | lifted | published | held only for its group's composed check, which has since passed or warned for the heads that merged, and its own walk still open |
|
||||
|
||||
`delivered`, `failed`, `superseded`, `stopped`, `retired` and `covered` are final.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **The held deliveries end without a person.** When mesh-delivery with these rows is delivered, its first
|
||||
ticks deliver the hq merges and the module-adding merges, and cover the controller and catalogue merges
|
||||
that a later walk carried. Their D14 conditions clear at the self-check's next reading of `stalled`.
|
||||
- **A pull request no longer says an error for a change a later merge delivered.** The four stopped on 2026-10-09 keep their
|
||||
error: a final state is not undone. Their commit notes say why.
|
||||
- **Containment is the forge's answer, not a premise.** The trunk's protection is no longer relied on: the
|
||||
forge's compare decides. mesh-delivery gains one forge tool, which reads only.
|
||||
- **The forge's tool must be delivered before `covered` can be reached.** Until `gitea_contains` answers, every
|
||||
candidate is "not yet covered", and a held delivery stays held as before. Nothing ends wrongly while the forge
|
||||
module is behind.
|
||||
- **`covered` is only as complete as the walks' records.** A module the later walk held back for a person is
|
||||
not sent, so it does not count. A delivery whose modules were only partly carried stays held for a person.
|
||||
- **A module that runs on one machine only is never covered.** A walk records "the rest running it" only
|
||||
after its first machines passed and the rest were sent. A module on one machine has no rest, so its steps
|
||||
never say it reached every machine. mesh-delivery itself, on the control node alone, is such a module: a
|
||||
held merge of it stays held for a person, as before this record. Reading "every machine running it" from
|
||||
the first machines when they are all the machines would close it, and is left for a later change.
|
||||
- **A group member waits longer, rather than being held too soon.** A composed check that never answers leaves
|
||||
the member in `published`. Its bound of 30 minutes lists it as stalled. That is the right owner for a check
|
||||
that does not answer.
|
||||
|
||||
**How it is checked.** mesh-delivery's tests, in `covered_test.go`:
|
||||
|
||||
- `TestAMergeThatMovesNothingIsDeliveredAtOnce`: builds nothing, or only a new module sent nowhere; merged
|
||||
before its check answered, on a failed check, or on a passing one. Each is delivered, and its status is a
|
||||
success.
|
||||
- `TestAMergeALaterDeliveredWalkCarriedIsCovered`: covered whether it was held first or not. It stays held
|
||||
when the later walk carried only one of its modules, merged before it, went into another branch, or failed,
|
||||
and when the forge says the later merge does not contain it. With the forge away it stays held, and is
|
||||
covered once the forge answers. The forge is asked once per pair, and never for a pair merge order rules
|
||||
out. Its status is `success covered: a later merge that contains it was delivered` and names the commit
|
||||
that covered it, never an error.
|
||||
- `TestACoveredGroupMemberLetsTheNextOneGo`: the member after a covered one is let go, and the group ends
|
||||
delivered.
|
||||
- `TestHeldDeliveriesReadBackCloseByThemselvesOnce`: deliveries held before these rows existed are read back
|
||||
from the store and end delivered or covered, or stay held when not covered. A merge held without a passing
|
||||
check is covered by a later delivery whose own check passed, and stays held when that check did not pass. It
|
||||
stays held when the later delivery's walk was of another commit or another branch, or sent its module to its
|
||||
first machine only. A second restart moves nothing.
|
||||
- The forge module's `test/checks.test.ts`: a compare with no commits ahead is contained, one with some is
|
||||
not, an answer without a count is no answer, and only a sha reaches the compare's path. The forge module is
|
||||
TypeScript. Its tests, and a type-check of every source, are run on the build seat by the catalogue's
|
||||
TypeScript part of its check, `merge-check-typescript.sh` (mesh-catalog #151), in the TypeScript toolchain
|
||||
with the SDK it holds. Before #151 no gate ran them: the catalogue's check ran only Go.
|
||||
- `TestAGroupMemberWaitsForItsComposedCheckToAnswer` and `TestAMemberHeldForItsComposedCheckIsLiftedWhenItPasses`:
|
||||
a pass or a warning lifts the member; a failure, or a pass for other heads, keeps it held.
|
||||
|
||||
The table's own tests take and refuse every new row. They also check that the only way to `published` other
|
||||
than a ready delivery's merge is *lifted* from `held`, and that nothing observed ends a held delivery as
|
||||
stopped or failed.
|
||||
|
||||
## References
|
||||
|
||||
- [Issue 319](../04-ISSUES/319-a-held-delivery-a-later-commit-superseded-has-no-end-but-release/00-report.md) and
|
||||
[issue 316](../04-ISSUES/316-a-group-member-held-before-its-composed-check-answered-stayed-held-after-it-passed/00-report.md),
|
||||
their reports and diagnoses.
|
||||
- [ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md),
|
||||
decisions 2, 4 and 9; [ADR 0250](0250-a-merge-heard-only-from-the-buss-history-is-retired-and-owed-work-the-forge-cannot-do-ends.md),
|
||||
the last state added to the table.
|
||||
- [Issue 300](../04-ISSUES/300-a-new-module-is-not-built-at-its-merge/00-report.md), why a new
|
||||
module opens no walk.
|
||||
- [Design 47](../03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md), *The state table* and *A
|
||||
delivery group*, which carry these rows.
|
||||
- mesh-catalog #150, which builds it, and the forge module's `gitea_contains` with it; mesh-catalog #151,
|
||||
which has the catalogue's check run its TypeScript modules.
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-09
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md
|
||||
---
|
||||
|
||||
# 271. A send held for the bus's planned step is told to the operator at once, and a change that needs it says so before it merges
|
||||
|
||||
## Context
|
||||
|
||||
A new build of the bus is a **planned step**
|
||||
([ADR 0236](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md),
|
||||
[to-be 45](../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md) §8). Only a person's
|
||||
`mesh-controller.bus upgrade` moves it. Until then, every send to the bus's machine is refused, because such a
|
||||
send would carry the new bus with it. The controller's `sendToEach` refuses the send whole, and a walk keeps
|
||||
the refusal as its note and tries again on the next tick.
|
||||
|
||||
On 2026-10-08 a catalogue merge built a new bus for the control-node
|
||||
([issue 336](../04-ISSUES/336-a-send-held-for-the-bus-step-raises-nothing/00-report.md)). For 28 minutes every
|
||||
send to that machine was held: the controller's own walk, the node-engine's, and builds kept back for a gate.
|
||||
`mesh-controller.conditions` listed nothing about it. The operator found the hold only because a walk looked
|
||||
stuck, and asked where the approve button was.
|
||||
|
||||
Three things were missing, read from the controller's code
|
||||
([diagnosis](../04-ISSUES/336-a-send-held-for-the-bus-step-raises-nothing/01-diagnosis.md)):
|
||||
|
||||
- the refusal reaches only the walk's note, which nobody reads unless they already ask `plans`;
|
||||
- the only watchdog that could fire, S3, fires no sooner than 30 minutes in. Its words say the delivery is late
|
||||
and that nothing is lost. They name neither the bus nor the verb, and its resolver is not the operator;
|
||||
- the merge gate and the delivery plan were green. The plan said "a bus step", but not that merging would
|
||||
stop every later send to the bus's machine until a person acts.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Lower S3's bound for a walk refused for the bus.** Rejected: it would still say lateness, about one walk,
|
||||
in words that name neither the cause nor the verb. A wait for a person is not a walk running late.
|
||||
2. **Raise it from the self-check's probe DB, which already says a running bus step.** Rejected: the probe reads
|
||||
only a step that has started, and it runs on the self-check's interval. The refusal happens in the walk,
|
||||
which the watchdogs read every half minute.
|
||||
3. **Let the walk send everything but the bus, and keep the bus at the build its machine runs.** Not decided
|
||||
here. The diagnosis thinks it may now be possible, because every send keeps a recorded module at the build
|
||||
its machine was last sent. That has to be checked against the bus's user list, which issue 249 sends first in
|
||||
the same declaration. It would not remove the need to tell the operator: the step still waits for a person.
|
||||
4. **Hold the merge for the operator's word when it builds a new bus.** Rejected for now. It would block a
|
||||
change on a decision the operator can take after the merge as well. Asking the operator from the merge gate
|
||||
belongs with the asks of ADR 0259 (an ask is a message on the bus that any channel answers), which is still
|
||||
a pull request and not built.
|
||||
5. **A condition of its own, raised at the first refusal, and a line in the delivery plan** — chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **Row S17 of the signals table: a send held for the bus's planned step is told to a person.** On the first
|
||||
watchdog tick after a walk's send was refused for the bus, the controller raises
|
||||
`bus.<bus module>.step-waiting`, kind `bus-step-waiting`. It is a warning, and its resolver is the operator.
|
||||
Its summary names:
|
||||
- the machines whose bus the new build would replace;
|
||||
- each walk that waits, with its repository, commit and the modules of its tier;
|
||||
- the bus build, from and to;
|
||||
- since when: the first refusal this controller saw, or the save that kept the refusal after a restart;
|
||||
- the call that ends it, as the operator makes it through the mesh MCP server:
|
||||
`mesh_call mesh-controller.bus {"upgrade": "true", "why": "…", "reversible" or "irreversible": "true"}`.
|
||||
|
||||
Its plain words say that sends to the machine wait for a bus upgrade, what waits, for how long, and that the
|
||||
operator starts the upgrade from the mesh MCP server. It clears once the bus's machine has been sent the build the
|
||||
mesh holds, that is, once the step was taken, whatever the walks' notes still say. From then on no send is
|
||||
refused for the bus, so nothing waits for a person; whether the new bus came up healthy is the step's own
|
||||
condition, `bus-maintenance` and then `bus-upgrade-failed` (probe DB), not this one. A walk whose note names
|
||||
an older bus build than the one held now does not count.
|
||||
2. **S3 leaves out a walk held only by the bus's step**, as it leaves out a walk under a paused build seat. The
|
||||
wait is said once, as itself, never as lateness.
|
||||
3. **The delivery plan says it before the merge.** When a change builds the module that provides the bus and
|
||||
that module runs somewhere, its plan has the step: "merging this needs a person's bus upgrade: nats is never
|
||||
sent by an ordinary send, and until the bus upgrade runs, nothing else is sent to <its machines> either —
|
||||
unless the new build turns out the same as the one running there (issue 280)". The plan cannot know that
|
||||
before the build, so it says so rather than staying silent. Its summary says "merging this needs a person's bus
|
||||
upgrade". The forge module writes that summary into the `mesh/merge-gate` status and the plan into the pull
|
||||
request's comment. It is said, not enforced: the gate's verdict does not change.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **The operator is told within half a minute of the first refusal**, not when a walk looks stuck. The
|
||||
condition says what to run.
|
||||
- **Merging a change that builds the bus is a known promise.** Its pull request says, before the merge, that a
|
||||
person's step follows and what waits until then.
|
||||
- **One more row in the signals table.** The generated test walks it like every other row.
|
||||
- **The condition is about the bus, not about one walk.** Several walks waiting for the same step are one
|
||||
condition, and the last one to be refused does not reopen it.
|
||||
- Turning the condition into an ask, whose yes starts the step with the operator's word recorded, waits for
|
||||
ADR 0259's asks to be built.
|
||||
|
||||
**How it is checked.** In the controller:
|
||||
|
||||
- the signals table's generated test for S17: with a new bus build waiting and no send refused, nothing is
|
||||
raised; with a refused walk, the condition is raised; when the signal returns, it clears;
|
||||
- `TestASendHeldForTheBusStepIsFoundFromTheWalksItHolds`: a walk refused for the bus is found, and since when.
|
||||
A refusal for another reason, a refusal for an older bus build, a bus that already runs the build held, and a
|
||||
walk waiting for its delivery's word are not;
|
||||
- `TestASendHeldForTheBusStepIsSaidAtOnceAndNotAsLateness`: the condition's key, kind and operator resolver;
|
||||
its summary names the machine, the module, the builds from and to, the verb and since when; its words are
|
||||
plain. S3 says nothing of the walk past its bound, and still raises for a walk held for something else. Through
|
||||
the keeper, the condition opens at the first tick and clears when the step was taken;
|
||||
- the replay `TestReplay336`: a walk of the node-engine refused for a new bus build on the control-node raises
|
||||
the condition on the next watchdog tick, through the store, the plans and the watchdogs' own gathering. It
|
||||
clears once the machine is sent the new bus. It fails on the controller before the fix and passes on it;
|
||||
- `TestAChangePlanSaysWhatEachMachineReceives`: a change that builds the bus says "merging this needs a
|
||||
person's bus upgrade", the machine, and the verb.
|
||||
|
||||
## References
|
||||
|
||||
- [Issue 336](../04-ISSUES/336-a-send-held-for-the-bus-step-raises-nothing/00-report.md), its report and
|
||||
diagnosis.
|
||||
- [ADR 0236](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md),
|
||||
the bus as a planned step; [ADR 0227](0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md)
|
||||
rule 4, no failure is silent;
|
||||
[ADR 0254](0254-a-wait-for-a-persons-new-login-is-not-a-gates-failure-and-each-module-of-a-send-keeps-its-own-pass.md),
|
||||
the precedent of a wait for a person said as its own condition.
|
||||
- [To-be 45](../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md) §3, the signals table, and §8, the
|
||||
bus as a planned step, which carry this row.
|
||||
- mesh-controller #173, which builds it.
|
||||
@@ -2,8 +2,9 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-controller, mesh-host, mesh-tools, mesh-catalog, mesh-sdk, mesh-lab]
|
||||
updated: 2026-10-08
|
||||
updated: 2026-10-09
|
||||
decisions:
|
||||
- 02-DECISIONS/0271-a-send-held-for-the-buss-planned-step-is-told-to-the-operator-at-once-and-a-change-that-needs-it-says-so-before-it-merges.md
|
||||
- 02-DECISIONS/0265-a-verb-that-reads-is-answered-on-the-serving-controllers-own-connection-and-every-connection-says-what-it-is.md
|
||||
- 02-DECISIONS/0264-a-message-a-consumer-gives-up-on-is-kept-until-a-person-delivers-it-again-or-drops-it.md
|
||||
- 02-DECISIONS/0261-an-assignment-made-while-its-modules-build-runs-is-kept-pending-and-the-controller-makes-it.md
|
||||
@@ -233,6 +234,7 @@ signal and asserts its condition. `doctor signals` shows, for every row, the age
|
||||
| S14 | facts snapshot exported | controller | when it moved, and daily | 2 days, or none kept by a controller up that long (Phase 5) | `facts-stale` | warning | — |
|
||||
| S15 | a hand act with a cause already recorded | hand-act log | each act | the second within 14 days; clears when fewer than two remain within 14 days | `healer-wanted` (names the cause, and the healer that was not enough where one answers it) | warning | — |
|
||||
| S16 | a walk waiting for its delivery's word is let go | mesh-delivery, through `deliver` (ADR 0239) | each merge whose walk waits | 30 min; urgent after 4 h | `waiting` (names the walk and `plans go`) | warning | — |
|
||||
| S17 | a send held for the bus's planned step is told to a person (ADR 0271) | controller's plan | each send refused because it would replace the bus outside its step | none: the first refusal; cleared once the bus's machine has been sent the build the mesh holds | `bus-step-waiting`, for the operator (names the machines, what waits, the bus build from and to, since when and the `mesh-controller.bus` call); S3 leaves such a walk out | warning | — |
|
||||
|
||||
The bus advisories (S9) cost one read-only subscription: the server already publishes them. The
|
||||
controller translates each into a condition naming the thing in the mesh's words, as the refused reply
|
||||
@@ -562,7 +564,10 @@ bus machine's backup holder backing the bus module up now ([ADR 0235](../../02-D
|
||||
or one a person took and names with `--snapshot-taken`; the step's bound is fifteen minutes; **no send
|
||||
reaches the bus's machine while a new bus build waits for it** — a push, a plan's send for another
|
||||
module, a healer's — after a plan's send restarted the bus unasked on 2026-10-06; a change of the bus's
|
||||
user list is reloaded in place, not a restart. *2026-10-06:* the snapshot
|
||||
user list is reloaded in place, not a restart. **A send held for the step is told at once** ([ADR 0271](../../02-DECISIONS/0271-a-send-held-for-the-buss-planned-step-is-told-to-the-operator-at-once-and-a-change-that-needs-it-says-so-before-it-merges.md)):
|
||||
S17 raises `bus.<module>.step-waiting` for the operator on the first refusal, and a change that builds the bus
|
||||
says in its delivery plan, and so in `mesh/merge-gate`'s description, that merging it needs a person's bus
|
||||
upgrade. *2026-10-06:* the snapshot
|
||||
and the way back from it exist — the bus image's own snapshot program, the same one the night's backup
|
||||
runs, and a restore that builds a new store beside the live one for a person to swap in
|
||||
([ADR 0235](../../02-DECISIONS/0235-the-bus-is-backed-up-by-its-own-snapshot-of-each-stream.md),
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-10-08
|
||||
updated: 2026-10-09
|
||||
decisions:
|
||||
- 02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md
|
||||
- 02-DECISIONS/0250-a-merge-heard-only-from-the-buss-history-is-retired-and-owed-work-the-forge-cannot-do-ends.md
|
||||
- 02-DECISIONS/0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md
|
||||
- 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
|
||||
@@ -72,14 +73,26 @@ allows.
|
||||
| ready | it passed; it waits to be merged | none | — |
|
||||
| rejected | it failed its check | none | — |
|
||||
| published | merged on the trunk its modules follow, its walk opened and asking nothing; it waits for its turn or its word | 30 minutes | `superseded` when a newer delivery took over its walk; `go` when its walk started |
|
||||
| held | it waits for a person | 1 day | nothing: it is the operator's |
|
||||
| held | it waits for a person; read again each tick, so it ends by itself when it moves nothing or a later merge covers it (ADR 0270) | 1 day | nothing: it is the operator's |
|
||||
| delivering | its walk runs; its builds are asked and registered tier by tier | 2 hours | `delivered`, `failed` or `superseded`, as the walk's record says |
|
||||
| delivered, failed, superseded, stopped, retired | final | — | — |
|
||||
| delivered, failed, superseded, stopped, retired, covered | final | — | — |
|
||||
|
||||
The transitions are those of ADR 0239 decision 2. A delivery that is merged while not `ready` goes to
|
||||
`held`. Its check did not pass, and only a person decides that it goes on. A delivery merged with no walk
|
||||
opened for it within ten minutes is held too, saying so: nothing it moves follows that branch, or the merge
|
||||
was not heard. Nothing waits silently.
|
||||
The transitions are those of ADR 0239 decision 2 and [ADR 0270](../../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md). A delivery that is
|
||||
merged while not `ready` goes to `held`. Its check did not pass, and only a person decides that it goes on. A
|
||||
delivery merged with no walk opened for it within ten minutes is held too, saying so: nothing it moves follows
|
||||
that branch, or the merge was not heard. Nothing waits silently.
|
||||
|
||||
**What needs no person ends by itself** (ADR 0270):
|
||||
|
||||
- *a merge that moves nothing* (its plan builds nothing, or only adds a module the controller builds and
|
||||
sends nowhere) is `delivered` as soon as it is merged, whatever its check said;
|
||||
- *a merge a later delivered merge contains* is `covered`: every module it moves was sent to every machine
|
||||
running it by the walk of a later merge into the same trunk, delivered on its own passing check, and the
|
||||
forge says that later merge commit contains it (`gitea_contains`, asked once per pair and kept). Merge
|
||||
order is only the first filter, and a forge that cannot answer means not yet covered. `mesh/delivery`
|
||||
says `covered: a later merge that contains it was delivered` as a success. A module that runs on one
|
||||
machine only gets no "rest" step and is never covered;
|
||||
- the covered row is tried before `go`, from `held` and from `ready` past the ten minutes.
|
||||
|
||||
**History is retired, not held** ([ADR 0250](../../02-DECISIONS/0250-a-merge-heard-only-from-the-buss-history-is-retired-and-owed-work-the-forge-cannot-do-ends.md)).
|
||||
A delivery made from the forge's word at its merge, heard fifteen minutes or more after that merge, with no walk
|
||||
@@ -108,7 +121,10 @@ controller's `plan-moved` event, never inferred from time.
|
||||
- **Check**: the members' heads composed together, one gate over every machine with all their definitions. It
|
||||
is asked by mesh-delivery through `delivery-check` when the group forms or a member's head moves, and
|
||||
posted on every member's head as `mesh/delivery-group`.
|
||||
- **Delivering**: in order. A member waits in `published` until the one before is `delivered`.
|
||||
- **Delivering**: in order. A member waits in `published` until the one before is `delivered` or `covered`.
|
||||
A member published while its group's composed check is asked and not answered waits there for the answer;
|
||||
one held because that check had not passed is *lifted* back to `published` once it passes or warns for the
|
||||
heads that merged and its own walk is still open (ADR 0270).
|
||||
- **A failed member**: the members after it are `stopped`, naming it. The members before it stay delivered.
|
||||
Its own builds were put back at its gate.
|
||||
|
||||
|
||||
+18
@@ -47,3 +47,21 @@ The group's order put it second. Open question 4 in the report asks whether that
|
||||
|
||||
**Located** in mesh-delivery: `groupTurns` holds on a composed check that has not answered, and neither
|
||||
`Checked` nor the table brings a held member back when the check later passes.
|
||||
|
||||
## 2026-10-09 — the fix, proposed
|
||||
|
||||
[ADR 0270](../../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)
|
||||
(proposed) answers open questions 1 and 2, and mesh-catalog #150 builds it:
|
||||
|
||||
- **Asked and not answered is a reason to wait, not to hold.** `groupTurns` leaves a member in `published` while
|
||||
its group's composed check is asked for the current heads and has no verdict.
|
||||
- **A hold whose reason is gone lifts, by observation.** A member held because its group's composed check had not
|
||||
passed goes back to `published` (the event *lifted*) once the check passes or warns for the heads that merged,
|
||||
while its own walk is still open. It then takes its turn in order.
|
||||
- A held member whose own walk was since taken over by a later walk is not lifted, since its walk can no longer
|
||||
deliver it. It is `covered` when that later walk was delivered and sent its module (issue 319).
|
||||
|
||||
Open question 3, whether `mesh/delivery-group` is required on the trunks, stays the operator's setting. Open
|
||||
question 4 is not touched.
|
||||
|
||||
**Accepted 2026-10-09.** The operator accepted the record named above; it is no longer proposed.
|
||||
|
||||
+40
@@ -107,3 +107,43 @@ remaining seven were left `held` for this issue's fix.
|
||||
|
||||
The seven show the two ends this issue asks for: four hold nothing to deliver, and three are covered by a
|
||||
later walk.
|
||||
|
||||
## 2026-10-09 — the fix, proposed
|
||||
|
||||
[ADR 0270](../../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)
|
||||
(proposed) gives both ends this issue asks for, by observation, and mesh-catalog #150 builds them in
|
||||
mesh-delivery:
|
||||
|
||||
- **A merge that moves nothing is delivered at once.** `MovesNothing()` no longer counts `Plan.New`: a module
|
||||
the controller builds and sends nowhere opens no walk, so there is nothing to wait for. A merge whose plan moves
|
||||
nothing goes from `proposed`, `checked`, `rejected` or `held` to `delivered`, before the row *merged unchecked
|
||||
→ held*.
|
||||
- **`covered`, a new final state: a later merge that contains it was delivered.** For every module its plan
|
||||
moves, a delivered delivery of the same repository and trunk, merged after it, delivered on its own passing
|
||||
check by the walk of its own merge commit, whose machine steps say the module reached every machine running
|
||||
it. The forge then says, through a new read-only tool `gitea_contains` (its compare of the two commits),
|
||||
whether the later merge commit contains this one; merge order is only the first filter. A forge that cannot
|
||||
answer leaves it held. `mesh/delivery` says `covered: a later merge that contains it was delivered: …` as a
|
||||
success.
|
||||
- **Read on every tick**, so what is held now ends without an event.
|
||||
|
||||
This answers open question 1: mesh-delivery reads it from its own records and asks the forge for ancestry; the
|
||||
controller is not asked. Open question 3, why the controller's own merges opened no walk, is not answered by this fix; it no
|
||||
longer leaves anything held.
|
||||
|
||||
**How to verify after mesh-delivery is delivered with it.** Within a minute of its start:
|
||||
|
||||
1. `mesh-delivery.deliveries` with `state: held` lists none of the four hq merges, none of the module-adding
|
||||
merges (`builds new: …, sent nowhere`), and none of the five controller merges held for "no walk opened".
|
||||
The hq and module-adding ones are `delivered`; the controller ones are `covered`.
|
||||
2. `mesh-delivery.show` of one of them, for example `novox/mesh-controller@b98fd0f396c8`, ends with a
|
||||
transition `held → covered (covered)` whose why names the later delivery and its walk.
|
||||
3. `mesh-delivery.checks` for its pull request shows `mesh/delivery` as `success covered: …`.
|
||||
4. The forge module is delivered in the same pull request. Until its `gitea_contains` answers, nothing is
|
||||
covered, and the held controller merges stay held.
|
||||
5. At the self-check's next reading, `mesh-controller.conditions` no longer lists their
|
||||
`delivery.<id>.stalled`.
|
||||
6. The two catalogue merges held for their group (issue 316) are covered only if a later delivered walk sent
|
||||
their module. One that stays held is a real question: its change may not run anywhere.
|
||||
|
||||
**Accepted 2026-10-09.** The operator accepted the record named above; it is no longer proposed.
|
||||
|
||||
@@ -58,3 +58,26 @@ running build and carry the rest, so a pending bus step would stop holding every
|
||||
control-node. That needs checking against the bus's user list, which [issue 249](../249-a-modules-new-state-is-refused-until-a-push-the-merge-did-not-make/00-report.md) sends first in the same
|
||||
declaration, before it is proposed as a fix. It does not replace the condition: the step still waits for a
|
||||
person, and the person must be told.
|
||||
|
||||
## 2026-10-09 — the fix, proposed
|
||||
|
||||
[ADR 0271](../../02-DECISIONS/0271-a-send-held-for-the-buss-planned-step-is-told-to-the-operator-at-once-and-a-change-that-needs-it-says-so-before-it-merges.md)
|
||||
(proposed) takes suggested fixes 1 and 3. mesh-controller #173 builds them:
|
||||
|
||||
- **Row S17**, `bus.<bus module>.step-waiting`: raised on the first watchdog tick after a walk's send was refused
|
||||
for the bus's planned step, while the new bus build would still replace the bus on its machine. It is a
|
||||
warning, for the operator. It names the machines, each waiting walk and the modules of its tier, the bus build
|
||||
from and to, since when, and the `mesh-controller.bus` call with `upgrade`. It clears once the bus's machine has
|
||||
been sent the build the mesh holds.
|
||||
- **S3 leaves out a walk held only by it.**
|
||||
- **The delivery plan says it before the merge**: "merging this needs a person's bus upgrade: … nothing else is
|
||||
sent to <machine> either", and its summary, which `mesh/merge-gate` carries, says the same.
|
||||
|
||||
The replay `TestReplay336` in the controller fails on the commit before the fix ("the first tick after the refusal
|
||||
told the operator nothing about the bus's step: 0 condition(s)") and passes on it. It still has to be registered
|
||||
in mesh-lab's replays register as R336 before this issue resolves with `replay: R336`.
|
||||
|
||||
Suggested fix 2 (an ask) waits for ADR 0259's asks. The guess about letting a send keep the bus at its running
|
||||
build and carry the rest is not checked here.
|
||||
|
||||
**Accepted 2026-10-09.** The operator accepted the record named above; it is no longer proposed.
|
||||
|
||||
Reference in New Issue
Block a user