ADR 0230: ten minutes as well as five passes, and mark-only where a provider cannot disable
Five passes are 25 s, shorter than a real hiccup; and a TypeScript provider's remove can destroy, so retiring never calls it. Every TypeScript provider is mark-only until it gains a retire that disables.
This commit is contained in:
+3
-3
@@ -63,9 +63,9 @@ has forgotten it.
|
||||
> **The mechanism changed — 2026-10-06, by [ADR 0230](0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md).**
|
||||
> What stands: a consumer the mesh stopped asking for is announced recovered. What moved: a consumer the
|
||||
> provider still holds is no longer withdrawn on the first pass that misses it; it is said recovered when
|
||||
> it is *retired*, after five passes or a person's approval. A provider says what it retires, approves,
|
||||
> re-enables and deletes on an event of its own, `provisioner.retirement`, permitted the same way as the
|
||||
> two events here.
|
||||
> it is *retired*, after five passes and ten minutes, or a person's approval. A provider says what it
|
||||
> retires, approves, re-enables and deletes on an event of its own, `provisioner.retirement`, permitted
|
||||
> the same way as the two events here.
|
||||
|
||||
**2. Every provider may say it, whatever its manifest lists.** The permission to publish the two events
|
||||
is derived for every module that receives contributions; no manifest declares them. A provider whose
|
||||
|
||||
+3
-3
@@ -137,9 +137,9 @@ an hour while the operator is told — and withdrawal destroys no data since iss
|
||||
> **Replaced in part — 2026-10-06, by [ADR 0230](0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md).**
|
||||
> The paragraph above no longer stands; the operator decided against releasing one consumer an hour.
|
||||
> A consumer the mesh stops asking for is now *retired* — disabled, reversibly, and marked to delete —
|
||||
> once the same result holds for five passes; a set of more than three, or more than half of those held,
|
||||
> waits for a person's `retire approve`; and only `cleanup delete` deletes. Every other decision in this
|
||||
> record stands. The *how it is checked* row for the brake is replaced by 0230's.
|
||||
> once the same result holds for five passes and ten minutes; a set of more than three, or more than
|
||||
> half of those held, waits for a person's `retire approve`; and only `cleanup delete` deletes. Every
|
||||
> other decision in this record stands. The *how it is checked* row for the brake is replaced by 0230's.
|
||||
|
||||
**What Phase 1 decided while building** — [issue 270](../04-ISSUES/270-phase-1-watches-signals-that-come-later-and-hears-only-the-controllers-faults/00-report.md)'s
|
||||
decisions 1 to 6: the condition history as a bucket of its own, the events' shape, a key's last token,
|
||||
|
||||
+38
-15
@@ -62,28 +62,49 @@ there.
|
||||
|
||||
- **Retired**: the mesh stopped asking for it. The provider **disables its access** — reversibly — and
|
||||
**marks its login and data "to delete", with when and why**. Nothing is deleted. Asked for again, the
|
||||
provider's ordinary create re-enables it at once, as it was, and clears the mark.
|
||||
provider's ordinary create re-enables it at once, as it was, and clears the mark. A provider that
|
||||
has no non-destructive way to disable does **not** fall back on its removal: it is **mark-only** —
|
||||
the consumer keeps its access, is marked retired and needing deletion, and is said and announced (§2).
|
||||
- **Deleted**: only through `cleanup delete`, after a person approved it.
|
||||
|
||||
**2. What "disable" means, per backend** — each the smallest reversible switch that stops the consumer
|
||||
reaching its data, keeping everything else:
|
||||
**2. What "disable" means, per provider kind** — each the smallest reversible switch that stops the
|
||||
consumer reaching its data, keeping everything else; or, where a provider has none, nothing at all:
|
||||
|
||||
| Provider | Retired is | Kept | Mark |
|
||||
|---|---|---|---|
|
||||
| postgres | the role set `NOLOGIN`, its open sessions ended | the database, its owner and grants, every byte; `CONNECT` is not revoked and the database still accepts connections, so the nightly dump still backs it up | the role's comment, a JSON object naming the consumer's machine, when and why |
|
||||
| keycloak, the identity provider | the client `enabled: false` — the server refuses its authorization and token requests | its secret, redirects, mappers and every other setting | the client's attributes `mesh.retired` and `mesh.retired-why` |
|
||||
| the TypeScript providers | what their `remove` already does since issue 241 (a key revoked, a login disabled, roles stripped, a mailbox disabled), until each adds a `retire` of its own | as issue 241 lists | none yet: a TypeScript provider does not list or delete retired consumers until its adapter can |
|
||||
| a TypeScript provider whose adapter has `retire` (and, if its create does not undo it, `reenable`) | what its `retire` disables | what its `retire` keeps | what its `retire` writes |
|
||||
| a TypeScript provider without `retire` — **mark-only** | **nothing changes in the backend: the consumer keeps its access** | everything | the loop's own record, "retired, access kept, needs deletion", said loudly, announced, shown by `cleanup list` |
|
||||
|
||||
**`remove` is never called to retire.** A TypeScript adapter's `remove` is what it did on withdrawal, and
|
||||
several destroy something a person has not decided to lose — the secrets vault unlinks the ledger file
|
||||
holding the secret, the public DNS provider deletes the record, the message brokers delete the user. So
|
||||
an adapter without a non-destructive `retire` is mark-only, and `remove` is reached only as the delete of
|
||||
a provider without one of its own, through a person's `cleanup delete`. Today **every TypeScript provider
|
||||
is mark-only**, because none has a `retire` yet: redis, mosquitto, minio, umami, mssql, mongodb,
|
||||
influxdb, the secrets vault (mesh-vault), mailu, gitea and cloudflare-dns. Each leaves mark-only by
|
||||
adding a `retire` that disables — the SQL Server, document-store, object-store, mail and forge providers
|
||||
already have such a switch in their `remove` (disable the login, strip the roles, revoke the key,
|
||||
disable the mailbox, prohibit the login), moved into `retire`.
|
||||
|
||||
A mark-only retirement is kept in the provider's process, not its backend: a restart forgets it, and the
|
||||
consumer, still reachable, is no longer listed. That is the cost of a provider that cannot disable, and
|
||||
the reason to give each a `retire`.
|
||||
|
||||
A database renamed aside — by postgres's `postgres_retire_database` tool or by hand, the
|
||||
`<name>_deleted_<date>` form — is listed beside the retired consumers, retired since that date, so a
|
||||
person sees it and can delete it.
|
||||
|
||||
**3. Stable removals.** A provider retires a consumer only after it has seen **the same set** of
|
||||
consumers no longer asked for in **five consecutive passes** that read the contributions file. A pass
|
||||
that could not read it is not a result and starts the count again; so does a different set. At the loop's
|
||||
pass interval of **five seconds**, retirement comes **twenty seconds** after the first pass that saw the
|
||||
set — twenty to twenty-five seconds after the file changed. **Additions and changes to an existing
|
||||
consumer — create, rotate — act on the first pass and are never delayed.**
|
||||
consumers no longer asked for **both** in **five consecutive passes** that read the contributions file
|
||||
**and** for at least **ten minutes** since the first of them (the constant `StableFor` in the Go loop,
|
||||
`STABLE_FOR_MS` in the SDK's). At the loop's pass interval of five seconds, five passes alone are
|
||||
twenty-five seconds — shorter than a controller restart, a store reconnecting or a file half written —
|
||||
so the ten minutes are what a real hiccup has to outlast, and the five passes keep one slow pass from
|
||||
counting as agreement. A pass that could not read the file is not a result and starts both again; so does
|
||||
a different set. **Additions and changes to an existing consumer — create, rotate — act on the first pass
|
||||
and are never delayed.**
|
||||
|
||||
**4. Too many is a person.** A stable set of **more than three consumers, or of more than half of those
|
||||
the provider holds where it holds more than one**, retires nothing. The provider **waits**: it says so in
|
||||
@@ -152,11 +173,12 @@ every consumed kind's contract — stands unchanged.
|
||||
point; the thirty-day condition is what keeps it from being forgotten.
|
||||
- **A person who rejects must come back.** A rejected set is kept active and said as a warning until the
|
||||
mesh asks for it again or someone approves it.
|
||||
- **The TypeScript providers get the stable count and the threshold** from the SDK's loop on their next
|
||||
build (the SDK's provisioner, every catalogue provider's range accepts it), but a set over the bound
|
||||
there waits with no way to approve it until each provider passes its module name and an announcer to
|
||||
the loop, and they cannot list or delete what they retired until their adapters can. Until then they
|
||||
are covered for safety and not for cleanup, and the record says so rather than claiming otherwise.
|
||||
- **The TypeScript providers get the stable count, the ten minutes, the threshold and mark-only** from
|
||||
the SDK's loop on their next build (every catalogue provider's range accepts it). Mark-only is safe —
|
||||
nothing is destroyed — and weak: the consumer keeps its access, and a restart forgets the mark. A set
|
||||
over the bound there waits with no way to approve it until each provider passes its module name and an
|
||||
announcer to the loop. Until each adds a `retire` and that wiring, they are covered for safety and not
|
||||
for disabling or cleanup, and the record says so rather than claiming otherwise.
|
||||
- **The Go loop is still two identical copies**, in postgres and keycloak,
|
||||
held together by a test — now over three files. Its home is the Go SDK ([ADR 0039](0039-what-the-sdk-holds-and-refuses.md));
|
||||
moving it there means a tagged SDK release before the catalogue can use it, a separate change.
|
||||
@@ -168,12 +190,13 @@ every consumed kind's contract — stands unchanged.
|
||||
|
||||
| What | Checked by |
|
||||
|---|---|
|
||||
| a transient empty list for four passes retires nothing; five stable passes retire; an unreadable pass and a changed set restart the count; additions and changes are not delayed | the catalogue's shared `retirement_test.go` (identical in both Go providers) and the SDK's `retire.test.ts` |
|
||||
| a transient empty list for four passes retires nothing; five passes in twenty-five seconds retire nothing, the same set held ten minutes does; ten minutes in fewer than five passes do not; an unreadable pass and a changed set restart the count; additions and changes are not delayed | the catalogue's shared `retirement_test.go` (identical in both Go providers) and the SDK's `retire.test.ts` |
|
||||
| over the bound waits, is announced and said, again every fifteen minutes; approve takes only the exact set; reject keeps it and is not asked again; a set asked for again settles | the same tests |
|
||||
| retired is disabled with data intact; asked for again it is enabled as it was | postgres's `live_test.go` and keycloak's `live_retire_test.go`, against throwaway servers |
|
||||
| delete removes only that consumer, never an active or asked one | the same live tests, and `retirement_test.go` |
|
||||
| a restart retires what the backend holds unasked and adopts what it finds disabled | `retirement_test.go`, the provider's inventory tests |
|
||||
| the conditions, verbs, hand acts and the thirty-day probe | the controller's tests of the event, the verbs against a fake provider on a real bus, and the probe |
|
||||
| an adapter without `retire` is mark-only: `remove` is not called on retirement, the consumer is marked with its access kept and said loudly, and `remove` runs only on a person's delete; `reenable` runs before create for a retired consumer asked for again | the SDK's `retire.test.ts` and `sdk.test.ts` |
|
||||
| the two Go copies agree | `harness_same_test.go` in each provider, over the three files |
|
||||
| live, after rollout | `cleanup list` names the adopted and set-aside entries; `conditions` shows no `retire-waiting` on a mesh nobody is changing |
|
||||
|
||||
|
||||
@@ -252,8 +252,10 @@ failing word per provider, machine and consumer, and `status` names each one unt
|
||||
A consumer is active, retired or deleted
|
||||
([ADR 0230](../../02-DECISIONS/0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md)).
|
||||
The provider's loop retires one only after the same set has gone unasked in five consecutive passes
|
||||
that read the contributions file; retiring disables its access reversibly and marks it, in the
|
||||
provider's own backend, with when and why; asked for again, the ordinary create re-enables it. A set of
|
||||
that read the contributions file and for ten minutes; retiring disables its access reversibly and marks
|
||||
it, in the provider's own backend, with when and why — or, for a provider with no way to disable, only
|
||||
marks it, its access kept, and never calls its removal; asked for again, the ordinary create re-enables
|
||||
it. A set of
|
||||
more than three, or of more than half of those held, waits for a person. Every provider serves four
|
||||
tools for this — `provisioner_retirement`, `provisioner_retire_approve`, `provisioner_retire_reject`,
|
||||
`provisioner_delete` — and says `provisioner.retirement`, permitted like the two events above. The
|
||||
|
||||
@@ -460,7 +460,7 @@ bound corrected in the table.
|
||||
| mesh-controller | the lease and epoch (§6); plans written by compare-and-set; a report kept by sequence; abandoned calls marked; S12, S13, D5; the writers table enforced at grant composition; contract tests for every consumed subject and the check listing them; the empty-on-error lint |
|
||||
| mesh-host | one apply queue; the report sequence kept on disk; epoch refusal reported; the `report` verb; the contract tests and lint |
|
||||
| mesh-tools | refusing an unreadable or unknown input by name; the lint |
|
||||
| mesh-catalog, mesh-sdk | retirement in the providers' loop (ADR 0230, replacing ADR 0229's brake): a consumer no longer asked for is retired — disabled, marked, its data kept — only once the same result holds for five passes; a set of more than three, or more than half of those held, waits for a person and raises a condition; the four retirement tools on every provider |
|
||||
| mesh-catalog, mesh-sdk | retirement in the providers' loop (ADR 0230, replacing ADR 0229's brake): a consumer no longer asked for is retired — disabled, marked, its data kept; or, by a provider that cannot disable, only marked, its access kept — only once the same result holds for five passes and ten minutes; `remove` is never called to retire; a set of more than three, or more than half of those held, waits for a person and raises a condition; the four retirement tools on every provider |
|
||||
| mesh-lab | R1, R2, R6 |
|
||||
|
||||
**Done when:** R1 ends with nothing from the stale epoch applied, R2 with one report of the newest
|
||||
@@ -476,12 +476,14 @@ brake is said in its journal only.
|
||||
|
||||
**Amended 2026-10-06** ([ADR 0230](../../02-DECISIONS/0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md),
|
||||
the operator's decision): the brake's hourly release is replaced by retirement — three states (active,
|
||||
retired, deleted), five stable passes before a consumer is retired, a person for more than three or more
|
||||
retired, deleted), five stable passes and ten minutes before a consumer is retired, a provider that
|
||||
cannot disable marking only (every TypeScript provider today), a person for more than three or more
|
||||
than half, `retire approve|reject`, `cleanup list|delete`, `retirement` events and D11. Built on branches
|
||||
in mesh-catalog (both Go providers), mesh-sdk (0.1.12) and mesh-controller, not yet merged. **Not yet:**
|
||||
the TypeScript providers passing their module name and an announcer to the loop, without which a set
|
||||
over the bound there waits with no way to approve it, and adapters that list and delete what they
|
||||
retired.
|
||||
the TypeScript providers giving their adapters a `retire` that disables (until then each is mark-only:
|
||||
its retired consumers keep their access), passing their module name and an announcer to the loop,
|
||||
without which a set over the bound there waits with no way to approve it, and adapters that list and
|
||||
delete what they retired.
|
||||
|
||||
### Phase 3 — Healers
|
||||
|
||||
|
||||
Reference in New Issue
Block a user