From da136ab40e4d6cbcc6523e125c6b3ff8cc6920e3 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 6 Oct 2026 14:35:48 +0200 Subject: [PATCH] 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. --- ...mer-is-a-problem-the-controller-reports.md | 6 +-- ...och-a-machine-is-sent-once-it-reads-one.md | 6 +-- ...is-retired-and-deleted-only-by-a-person.md | 53 +++++++++++++------ 03-DESIGN/01-to-be/19-the-module-protocol.md | 6 ++- .../45-a-core-that-cannot-fail-silently.md | 12 +++-- 5 files changed, 55 insertions(+), 28 deletions(-) diff --git a/02-DECISIONS/0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md b/02-DECISIONS/0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md index e1290fe..291a2ef 100644 --- a/02-DECISIONS/0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md +++ b/02-DECISIONS/0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md @@ -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 diff --git a/02-DECISIONS/0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md b/02-DECISIONS/0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md index 1e67d48..e4b8fa6 100644 --- a/02-DECISIONS/0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md +++ b/02-DECISIONS/0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md @@ -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, diff --git a/02-DECISIONS/0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md b/02-DECISIONS/0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md index 6768866..de86704 100644 --- a/02-DECISIONS/0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md +++ b/02-DECISIONS/0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md @@ -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 `_deleted_` 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 | diff --git a/03-DESIGN/01-to-be/19-the-module-protocol.md b/03-DESIGN/01-to-be/19-the-module-protocol.md index c238bae..5b96b7b 100644 --- a/03-DESIGN/01-to-be/19-the-module-protocol.md +++ b/03-DESIGN/01-to-be/19-the-module-protocol.md @@ -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 diff --git a/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md b/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md index 4b022f1..c328a27 100644 --- a/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md +++ b/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md @@ -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