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).**
|
> **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
|
> 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
|
> 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,
|
> it is *retired*, after five passes and ten minutes, or a person's approval. A provider says what it
|
||||||
> re-enables and deletes on an event of its own, `provisioner.retirement`, permitted the same way as the
|
> retires, approves, re-enables and deletes on an event of its own, `provisioner.retirement`, permitted
|
||||||
> two events here.
|
> 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
|
**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
|
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).**
|
> **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.
|
> 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 —
|
> 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,
|
> once the same result holds for five passes and ten minutes; a set of more than three, or more than
|
||||||
> waits for a person's `retire approve`; and only `cleanup delete` deletes. Every other decision in this
|
> half of those held, waits for a person's `retire approve`; and only `cleanup delete` deletes. Every
|
||||||
> record stands. The *how it is checked* row for the brake is replaced by 0230's.
|
> 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
|
**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,
|
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
|
- **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
|
**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.
|
- **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
|
**2. What "disable" means, per provider kind** — each the smallest reversible switch that stops the
|
||||||
reaching its data, keeping everything else:
|
consumer reaching its data, keeping everything else; or, where a provider has none, nothing at all:
|
||||||
|
|
||||||
| Provider | Retired is | Kept | Mark |
|
| 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 |
|
| 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` |
|
| 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
|
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
|
`<name>_deleted_<date>` form — is listed beside the retired consumers, retired since that date, so a
|
||||||
person sees it and can delete it.
|
person sees it and can delete it.
|
||||||
|
|
||||||
**3. Stable removals.** A provider retires a consumer only after it has seen **the same set** of
|
**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
|
consumers no longer asked for **both** in **five consecutive passes** that read the contributions file
|
||||||
that could not read it is not a result and starts the count again; so does a different set. At the loop's
|
**and** for at least **ten minutes** since the first of them (the constant `StableFor` in the Go loop,
|
||||||
pass interval of **five seconds**, retirement comes **twenty seconds** after the first pass that saw the
|
`STABLE_FOR_MS` in the SDK's). At the loop's pass interval of five seconds, five passes alone are
|
||||||
set — twenty to twenty-five seconds after the file changed. **Additions and changes to an existing
|
twenty-five seconds — shorter than a controller restart, a store reconnecting or a file half written —
|
||||||
consumer — create, rotate — act on the first pass and are never delayed.**
|
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
|
**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
|
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.
|
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
|
- **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.
|
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
|
- **The TypeScript providers get the stable count, the ten minutes, the threshold and mark-only** from
|
||||||
build (the SDK's provisioner, every catalogue provider's range accepts it), but a set over the bound
|
the SDK's loop on their next build (every catalogue provider's range accepts it). Mark-only is safe —
|
||||||
there waits with no way to approve it until each provider passes its module name and an announcer to
|
nothing is destroyed — and weak: the consumer keeps its access, and a restart forgets the mark. A set
|
||||||
the loop, and they cannot list or delete what they retired until their adapters can. Until then they
|
over the bound there waits with no way to approve it until each provider passes its module name and an
|
||||||
are covered for safety and not for cleanup, and the record says so rather than claiming otherwise.
|
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,
|
- **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));
|
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.
|
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 |
|
| 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 |
|
| 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 |
|
| 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` |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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
|
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)).
|
([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
|
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
|
that read the contributions file and for ten minutes; retiring disables its access reversibly and marks
|
||||||
provider's own backend, with when and why; asked for again, the ordinary create re-enables it. A set of
|
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
|
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`,
|
tools for this — `provisioner_retirement`, `provisioner_retire_approve`, `provisioner_retire_reject`,
|
||||||
`provisioner_delete` — and says `provisioner.retirement`, permitted like the two events above. The
|
`provisioner_delete` — and says `provisioner.retirement`, permitted like the two events above. The
|
||||||
|
|||||||
@@ -446,7 +446,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-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-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-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 |
|
| mesh-lab | R1, R2, R6 |
|
||||||
|
|
||||||
**Done when:** R1 ends with nothing from the stale epoch applied, R2 with one report of the newest
|
**Done when:** R1 ends with nothing from the stale epoch applied, R2 with one report of the newest
|
||||||
@@ -462,12 +462,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),
|
**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,
|
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
|
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:**
|
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
|
the TypeScript providers giving their adapters a `retire` that disables (until then each is mark-only:
|
||||||
over the bound there waits with no way to approve it, and adapters that list and delete what they
|
its retired consumers keep their access), passing their module name and an announcer to the loop,
|
||||||
retired.
|
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
|
### Phase 3 — Healers
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user