mesh/merge-gate pass: builds new: modules/mesh-issues, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
From the review: a taken key that cannot be read back, or a write never confirmed, now tells the caller to get the number before opening again; an owner entry naming a module is owned only by that module registered; link targets get the secret guard; line separators are refused in one-line fields; the acceptance test now proves the counter's own updates were contended.
105 lines
6.5 KiB
Markdown
105 lines
6.5 KiB
Markdown
# mesh-issues
|
|
|
|
The issue tracker: the mesh numbers and tracks its issues in a module of its own (novox/hq ADR 0275,
|
|
to-be 53). An **issue** is a question about the mesh that someone must answer; this module holds the
|
|
mesh's issues and hands out their numbers. It holds the seat `issue-tracker`, once for the mesh, on the
|
|
control-node.
|
|
|
|
Built on the model of [hits](https://github.com/impire-io/hits), a headless issue tracker on NATS, studied
|
|
at release v0.7.0: its statuses, fields, append-only notes, typed links and compare-and-set numbering are
|
|
taken as ideas. It carries none of its code, under a licence that is not an open-source one.
|
|
|
|
## Verbs
|
|
|
|
Called as `issue-tracker.<verb>` through the mesh's tools; each says in its description what it does and
|
|
what it refuses.
|
|
|
|
| verb | takes | does |
|
|
|---|---|---|
|
|
| `open` | `title`, `report`, optionally `located-in`, `record` | checks the issue, then takes the next number and keeps it under that number; answers the number |
|
|
| `get` | `number` | the issue whole: fields, notes, links |
|
|
| `list` | optionally `status`, `owner`, `phrase`, `limit` | short rows, newest first: number, status, owner, title |
|
|
| `move` | `number`, `status`, and what that status needs | changes the status under the development cycle's rules |
|
|
| `note` | `number`, `text` | appends a note; a note is never edited |
|
|
| `link` | `number`, `kind`, `target`, optionally `remove` | adds or removes a typed link |
|
|
|
|
**The cycle's rules, at the write** (hq `00-META/checks/cycle.py`): a status is one of `open`,
|
|
`diagnosing`, `located`, `resolved`, `wontfix`; `located` needs `located-in`; `resolved` needs
|
|
`located-in` and `fixed-by`; a core issue (located in the controller, the node-engine, the tool runner,
|
|
the SDK, or the catalogue's bus, forge or build agent) resolves with `replay` or `replay-none`; `wontfix`
|
|
needs a `reason`. A refused change changes nothing and says which rule refused it.
|
|
|
|
**An issue with a folder in hq** (`record` at open: the folder's name, without the number) keeps its
|
|
status in that folder's report until the import of slice 3. The tracker holds its number, title, folder,
|
|
notes and links, and `move` refuses it, saying where its status lives.
|
|
|
|
**Every argument is read strictly.** One a verb does not take is refused, never dropped. Every field has a
|
|
size limit, a one-line field takes no control character, a folder's name is no path, and text shaped like
|
|
a credential (a private key, an API key, a forge or bus token, `password=…`) is refused: an issue is read by
|
|
every agent on the mesh. The shapes catch the ordinary mistake, not every secret.
|
|
|
|
## Numbers
|
|
|
|
One counter, the key `issue` of the state `counters`, advanced only by compare-and-set: the SDK's
|
|
`State(...).Increment` reads it and writes the next number only at the revision it read, so two openers —
|
|
in one instance or two — never take one number. A write another opener won is read and tried again, a
|
|
bounded number of times with a short random wait; past the bound the open is refused and takes no number.
|
|
|
|
- **A refused open takes no number**: an issue is checked before its number is taken.
|
|
- **A number is never handed out twice, and may be skipped.** A number taken and then not kept — the bus
|
|
failing between the counter and the issue — stays unused, and the answer says which. A write whose answer
|
|
was lost is recognised as its own when tried again, so it leaves no gap; when it cannot be confirmed either
|
|
way, the answer says the issue may have been written and to `get` that number before opening it again,
|
|
never that nothing was opened.
|
|
- **An issue is never overwritten.** It is kept by a write that is refused when the key exists; if the
|
|
counter ever hands out a number already kept, the open is refused loudly, the number stays unused, and
|
|
nothing is overwritten.
|
|
- **The counter starts once**, the first time a number is taken: at the setting `first-number`, or after
|
|
the highest issue already kept, whichever is higher, made only if it does not exist. The setting is never
|
|
read again, so a number never moves.
|
|
|
|
## State and events
|
|
|
|
Two states, declared in the manifest and made by the controller; the module never makes a bucket
|
|
(ADR 0201): `issues`, one key per issue by number, keeping 8 values per key; `counters`, one value per key.
|
|
Neither expires. One issue is kept in at most 256 KiB; a note that would pass it is refused.
|
|
|
|
Every change is an event: `mesh-issues.opened`, `.moved`, `.noted`, `.linked`, each with the number, what
|
|
changed, who changed it and when. The events are the audit trail. A change whose event the bus did not take
|
|
is kept, and its answer says the event was not said (`announced: false`).
|
|
|
|
## Who acted
|
|
|
|
ADR 0275 §5 records the caller from the bus credential, never from an argument. **The runtime does not yet
|
|
tell a module who called it**: a tool call reaches a bundle as a name and arguments, and the tool runner is
|
|
not told who called it either (hq issue 340). Until it does, every change is recorded as by `unproven`, no
|
|
verb takes an argument naming a caller, and none can be given one.
|
|
|
|
## Settings
|
|
|
|
```
|
|
{
|
|
"first-number": 365,
|
|
"owners": ["hq", "mesh-catalog", "mesh-catalog modules/nats", "mesh-controller", …]
|
|
}
|
|
```
|
|
|
|
- `first-number` — the number the first issue gets: the one after the highest issue number on hq's main
|
|
and every open branch at the moment the module is switched on. Read once.
|
|
- `owners` — the registered list `located-in` may name: repositories, and modules as
|
|
`mesh-catalog modules/<module>`. An entry of `located-in` is an owner, or an owner followed by a space or a
|
|
slash and where in it (`mesh-controller internal/broker`, `mesh-catalog modules/nats/module.json`). A module
|
|
must itself be registered: `mesh-catalog modules/<module>` is never taken as a place in `mesh-catalog`. It starts with the mesh's repositories and the modules
|
|
the catalogue held when this module was written; a module added since is added here through the
|
|
controller's settings verb.
|
|
|
|
## Tests
|
|
|
|
`go test ./...` in this directory. The acceptance — an agent opens an issue and gets the next number; two
|
|
agents opening at the same moment never get the same one — is
|
|
`TestFiftyOpensAtOnceFromTwoInstancesGetFiftyDistinctConsecutiveNumbers`: a JetStream server linked into
|
|
the test, two instances of this bundle (the test binary started again) speaking to a runtime played by the
|
|
test against the buckets the manifest declares, fifty opens at once, fifty distinct numbers from the first
|
|
in a row. It counts the writes lost to another opener, so it fails when the openers never collided. With
|
|
the counter written without its revision check, it fails.
|