Merge pull request 'ADR 0157: a build says what it does on the bus, as it happens' (#238) from feat/a-build-says-what-it-does into main
This commit was merged in pull request #238.
This commit is contained in:
@@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-01
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 157. A build says what it does on the bus, as it happens
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) made a build work submitted to a role: the
|
||||||
|
build-machine seat accepts a build and emits its outcome, one publish that reaches whoever asked, the
|
||||||
|
controller that records it and the catalogue that places it. Everything **between** the request and
|
||||||
|
the outcome — which command is running, how long it has taken, where it hung, the compiler's error,
|
||||||
|
the clone's refusal — lived in one container's standard error on one machine.
|
||||||
|
|
||||||
|
The night of 2026-09-30 showed the cost three times over. A build that failed showed a person one
|
||||||
|
line, the first of its failure, in the controller's `builds`; the rest was read with `docker logs` over
|
||||||
|
ssh, which the mesh's own rule forbids. A build that ran for minutes could not be told from one that
|
||||||
|
had hung. And the builder has no tools and emits nothing but the outcome, so the console
|
||||||
|
([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)) had nothing to show while a
|
||||||
|
build ran, and no viewer could be built on top of it. The operator's ask was plain: the builder is to
|
||||||
|
be fully transparent, with its log on the bus, so that a log viewer can be built on the bus later.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep the log in the outcome.** The result carries the whole log when the build ends. Nothing new
|
||||||
|
on the bus; nothing while the build runs; a viewer sees a build only once it is over, which is
|
||||||
|
exactly when the log matters least.
|
||||||
|
2. **A log store.** The builder writes its log to a file or a table and a tool reads it. A second
|
||||||
|
place to keep something the bus already carries, with its own retention, access and failure modes,
|
||||||
|
and no live reading without inventing a subscription over it.
|
||||||
|
3. **The log is the role's own events.** Two more events on the build-machine seat beside `built`:
|
||||||
|
`started` when work is taken, and `log.<build id>` for every line, published as the build runs.
|
||||||
|
The events stream already retains every role's events for a week, so a reader follows a build
|
||||||
|
live by subscribing its subject, or reads it back afterwards from the stream, and a viewer is a
|
||||||
|
subscriber and nothing more.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Option 3.** A build machine says everything it does on the bus, as the role it holds, under the
|
||||||
|
build's id, and the mesh keeps no other copy.
|
||||||
|
|
||||||
|
- The build-machine seat's protocol gains `started` and `log.*`. A holder may therefore publish
|
||||||
|
`mesh.seat.mesh-build-machine.event.started` and `…event.log.<id>`, and no other subject, by the
|
||||||
|
same derivation every seat's grants follow ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
|
||||||
|
The event's tail token is the build's id, so one build is one subject: a reader filters by subject
|
||||||
|
alone, on the server, and a week of other builds does not travel to show one.
|
||||||
|
- **Every line goes two ways**: to the machine's own standard error as before, and onto the bus. That
|
||||||
|
includes every command the builder runs, its duration and its failure, and on failure the command's
|
||||||
|
own output line by line — the compiler's words, the clone's refusal. A build machine with nobody
|
||||||
|
listening still prints; a listener reads the same lines.
|
||||||
|
- A line is a core publish, unawaited. The stream that holds the role's events captures it on its way
|
||||||
|
through, and a build does not slow to the pace of an acknowledgement per line. Each line carries a
|
||||||
|
sequence number from one, so a reader who joined late, or reads two copies, sees order and gaps.
|
||||||
|
`started` and `built` are published into the stream and awaited, because they are the two facts a
|
||||||
|
later reader must never find missing.
|
||||||
|
- **The mesh reads it back from the stream**, never from a record of its own: `builds --log <id>`, and
|
||||||
|
the same verb on the controller's seat ([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
|
||||||
|
reads one build's subject with a consumer that is gone when the reading is done. `builds` lists
|
||||||
|
each build's id beside it, and `build` says the id it asked with, so a person can follow.
|
||||||
|
- Nothing is declared by the builder module for this. The protocol is the seat's, seeded additively
|
||||||
|
into the store ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)), and the
|
||||||
|
holder's grant follows on the next composition of the broker node.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- A build is watchable while it runs, from anywhere on the mesh, with no access to the build machine.
|
||||||
|
The console's gap of 2026-10-01 — no live progress, no per-merge view — closes on the progress half;
|
||||||
|
the per-merge view is a reader over these subjects and the outcome, and is not built here.
|
||||||
|
- A log viewer on the bus is now a plain subscriber: live on `mesh.seat.mesh-build-machine.event.>`,
|
||||||
|
historical from the events stream filtered by a build's subject. NATS carries and retains; it does
|
||||||
|
not view. The `nats` command-line client can tail or replay a subject today; a viewer of our own is
|
||||||
|
later work and needs nothing more from the builder.
|
||||||
|
- The events stream grows by a build's log per build, for a week. A build is a few hundred lines; the
|
||||||
|
stream's limits are the bound, as for every other event, and a stream that fills drops the oldest.
|
||||||
|
- A line the bus did not take is lost, deliberately, and visible as a gap in the sequence. The outcome
|
||||||
|
is not affected: a build's result never depended on its narration.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The seat's holder may publish `started` and `log.<id>` and nothing wider | `TestTheBuildMachineMaySayWhatItDoesUnderTheBuildsId` (broker) |
|
||||||
|
| A build's lines reach a reader of its subject in order, and the stream holds them afterwards | `TestNatsABuildIsTakenAndItsOutcomeReachesEverybody` against a real server (link) |
|
||||||
|
| The seat verb `builds` with a build's id reads that build's log | `TestBuildsWithAnIdReadsThatBuildsLog` |
|
||||||
|
| Every command the builder runs is said, with its output on failure | `Command` speaks through the hook every build sets; the builder's tests still see the lines on standard error when nothing listens |
|
||||||
|
| Live: a build triggered after the roll-out is readable line by line through the console | done by hand after the merge of mesh-controller PR — see the design's note |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) — extended: the role now narrates as well as answers
|
||||||
|
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md) — the protocol and the verb
|
||||||
|
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — the console this feeds
|
||||||
|
- [Design 25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §3, [Design 18 — Building a module](../03-DESIGN/01-to-be/18-building-a-module.md)
|
||||||
|
- [Issue 176](../04-ISSUES/176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md) — the tool that starts a build and hears nothing; this gives it something to hear
|
||||||
@@ -170,6 +170,7 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0142** — [The mesh delivers its own components as binaries, not as container images](0142-the-mesh-delivers-its-own-components-as-binaries.md)
|
- **0142** — [The mesh delivers its own components as binaries, not as container images](0142-the-mesh-delivers-its-own-components-as-binaries.md)
|
||||||
- **0154** — [The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
- **0154** — [The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
||||||
- **0156** — [An artifact is what a build produces, the artifact store serves every kind, and its seat is named for its scope](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
|
- **0156** — [An artifact is what a build produces, the artifact store serves every kind, and its seat is named for its scope](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
|
||||||
|
- **0157** — [A build says what it does on the bus, as it happens](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)
|
||||||
|
|
||||||
### Its tiers, from the bottom up
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ code:
|
|||||||
- mesh-controller cmd/mesh-builder
|
- mesh-controller cmd/mesh-builder
|
||||||
- mesh-controller internal/builder
|
- mesh-controller internal/builder
|
||||||
- mesh-catalog modules/builder
|
- mesh-catalog modules/builder
|
||||||
updated: 2026-09-30
|
updated: 2026-10-01
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||||
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
||||||
- 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md
|
- 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md
|
||||||
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
||||||
@@ -267,6 +268,28 @@ ships one and wrong for code the mesh built, which has no unit until the mesh wr
|
|||||||
**Tools, hooks and consumers are not further modes**, which is the test of whether three is the
|
**Tools, hooks and consumers are not further modes**, which is the test of whether three is the
|
||||||
right number: they are loaded by a tool host, and a tool host is a process that stays up.
|
right number: they are loaded by a tool host, and a tool host is a process that stays up.
|
||||||
|
|
||||||
|
## A build says what it does, as it happens
|
||||||
|
|
||||||
|
*2026-10-01 — [ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md).*
|
||||||
|
|
||||||
|
A build machine narrates every build on the bus as the role it holds: `started` when it takes the
|
||||||
|
work, one `log.<build id>` event per line — every command it runs with its duration, every step of
|
||||||
|
the recipe, and on failure the command's own output, line by line — and `built` for the outcome as
|
||||||
|
before. The same lines still go to the machine's standard error, so a build machine with nobody
|
||||||
|
listening is as readable as it was; a listener reads the same lines, live, from anywhere on the mesh.
|
||||||
|
|
||||||
|
One build is one subject. A reader follows it by subscribing that subject and nothing else, and the
|
||||||
|
events stream keeps it for a week, so `builds --log <id>` — on the command line and as the
|
||||||
|
controller's seat verb through the console — reads it back afterwards. `builds` lists every build's
|
||||||
|
id beside it, and `build` says the id it asked with. The mesh keeps no second copy: the stream is the
|
||||||
|
log. A viewer of builds, when one is built, is a subscriber over these subjects and the outcome; the
|
||||||
|
builder needs nothing more for it.
|
||||||
|
|
||||||
|
*How it is checked:* the holder's grant is exactly `started`, `built` and `log.*` (broker test); a
|
||||||
|
build's lines reach a reader of its subject in order and the stream holds them afterwards (link test
|
||||||
|
against a real server); the seat verb with an id reads the log (controller test); and, live, a build
|
||||||
|
after the roll-out read line by line through the console.
|
||||||
|
|
||||||
## The builder compiles the languages the mesh is written in
|
## The builder compiles the languages the mesh is written in
|
||||||
|
|
||||||
*2026-09-29 —
|
*2026-09-29 —
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ code:
|
|||||||
- mesh-tools src/broker-amqp.ts (to be replaced)
|
- mesh-tools src/broker-amqp.ts (to be replaced)
|
||||||
- mesh-catalog modules/nats (to be written)
|
- mesh-catalog modules/nats (to be written)
|
||||||
- mesh-sdk src (the protocol's NATS binding, step 3)
|
- mesh-sdk src (the protocol's NATS binding, step 3)
|
||||||
updated: 2026-09-27
|
updated: 2026-10-01
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
- 02-DECISIONS/0106-the-bus-is-nats.md
|
||||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||||
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
||||||
@@ -135,7 +136,7 @@ Core NATS is at-most-once. Everything the mesh must not lose lives in a JetStrea
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| CONTROL | `mesh.control.>` except `alive` (a build's outcome moved to its seat, ADR 0121) | work queue, one consumer (the controller), explicit ack | the store-window guarantee ([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)): the controller `nak`s with a delay while its store is away and the message is redelivered; nothing is dropped |
|
| CONTROL | `mesh.control.>` except `alive` (a build's outcome moved to its seat, ADR 0121) | work queue, one consumer (the controller), explicit ack | the store-window guarantee ([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)): the controller `nak`s with a delay while its store is away and the message is redelivered; nothing is dropped |
|
||||||
| NODES | `mesh.node.>` | last per subject | one declaration per node, always the newest |
|
| NODES | `mesh.node.>` | last per subject | one declaration per node, always the newest |
|
||||||
| EVENTS | `mesh.mod.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream) |
|
| EVENTS | `mesh.mod.*.event.>` and `mesh.seat.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream). *2026-10-01:* a build's whole log is here too, as the build-machine seat's `log.<build id>` events ([ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)) — one subject per build, a week of retention, read back by `builds --log <id>` with a consumer that is gone when the reading is done |
|
||||||
|
|
||||||
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
|
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
|
||||||
call is a timeout the caller already handles.
|
call is a timeout the caller already handles.
|
||||||
|
|||||||
Reference in New Issue
Block a user