ADR 0230: a consumer the mesh stops asking for is retired, and deleted only by a person
The operator's decision replaces ADR 0229's hourly release: the mesh no longer finishes alone what its bound exists to question. To-be 45 and the module protocol amended; 0229 and 0224 point to where their mechanism moved.
This commit is contained in:
+7
@@ -60,6 +60,13 @@ has forgotten it.
|
||||
- **No secret travels.** The error is the provider's own text with the consumer's password removed, as
|
||||
its log line already is.
|
||||
|
||||
> **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.
|
||||
|
||||
**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
|
||||
manifest forgot them would otherwise have its announcement refused by the bus and fail as silently as
|
||||
|
||||
+7
@@ -134,6 +134,13 @@ let go every hour, said. A consumer asked for again is kept. **The brake stops t
|
||||
intention**: an unassignment of many completes without a hand, a mistaken one costs at most one consumer
|
||||
an hour while the operator is told — and withdrawal destroys no data since issue 241.
|
||||
|
||||
> **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.
|
||||
|
||||
**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,
|
||||
the probe DW, which deleted consumers are said, and the bounds the design left to the build — is taken
|
||||
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-06
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md
|
||||
---
|
||||
|
||||
# 230. A consumer the mesh stops asking for is retired, and deleted only by a person
|
||||
|
||||
## Context
|
||||
|
||||
**This is the operator's decision, taken on 2026-10-06**, the day the withdrawal brake of
|
||||
[ADR 0229](0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md)
|
||||
was built. The model below — the three states, the stable result, the threshold that hands the act to a
|
||||
person, the cleanup verbs, the thirty-day reminder — is the operator's direction, recorded here as given.
|
||||
What the record adds is the evidence it rests on and the facts the build had to settle.
|
||||
|
||||
**What the brake did.** A provider's loop ([ADR 0224](0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md),
|
||||
in the Go providers and the SDK's) that would withdraw more than one consumer in a pass, or more than
|
||||
half of those it held, withdrew nothing and announced each kept consumer as failing; then, while the mesh
|
||||
went on not asking, it let one go every hour, with no person involved. So a mistaken unassignment of
|
||||
seven consumers still completed by itself in seven hours, and a person who did not read `status` in that
|
||||
time lost all seven.
|
||||
|
||||
**What withdrawal does today**, since [issue 241](../04-ISSUES/241-one-unreadable-grants-file-dropped-every-database-on-the-control-node/00-report.md):
|
||||
nothing a provider withdraws is destroyed in the providers that issue names — the relational database
|
||||
locks the role and keeps the database; the SQL Server provider disables the login; the document store
|
||||
strips the user's roles; the object store revokes the key and keeps the bucket; the mail server disables
|
||||
the mailbox; the forge prohibits the login; the analytics site is kept. But three things were still
|
||||
wrong:
|
||||
|
||||
- **The identity provider deleted the client.** Its withdrawal was a `DELETE` of the consumer's client,
|
||||
outside issue 241's list; the secret, redirects and mappers went with it.
|
||||
- **Nothing recorded that a withdrawal happened, or when.** A locked role is indistinguishable from one
|
||||
an operator locked by hand. On the home server one consumer's login has been locked by an earlier
|
||||
withdrawal, its database kept, with no record of when or why; on the control node six databases set
|
||||
aside during issue 241's recovery sit under a dated name, which is the only record of them.
|
||||
- **A restart forgot.** The loop withdrew only what *this process* had made. A consumer unassigned while
|
||||
its provider was down was never withdrawn at all — its login stayed open, indefinitely.
|
||||
|
||||
**And nothing ever deleted anything**, so withdrawn data accumulates with no surface that says it is
|
||||
there.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep the hourly release.** Rejected by the operator: it lets the mesh finish, unattended, an act the
|
||||
bound exists to question. Many changes at once means a person is at work, and that person can confirm
|
||||
once.
|
||||
2. **Delete after a grace period.** Rejected: a timer is the mesh acting alone again, only later; the
|
||||
loss in issue 241 was data nobody had decided to lose.
|
||||
3. **Withdraw at once, below the bound, as before.** Rejected: one pass is one read of one file, and
|
||||
issue 241 was one bad read. A result has to hold before it is acted on.
|
||||
4. **Three states — active, retired, deleted — where retiring is reversible and automatic only when
|
||||
stable and small, and deleting is always a person's act through the controller, executed by the
|
||||
provider.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. A consumer is ACTIVE, RETIRED or DELETED.**
|
||||
|
||||
- **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.
|
||||
- **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:
|
||||
|
||||
| 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 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.**
|
||||
|
||||
**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
|
||||
its journal and announces it, the controller raises an **urgent** condition listing what would be
|
||||
retired, and it waits for `retire approve <node> <provider-module>` or `retire reject <node>
|
||||
<provider-module>`. **An unassignment the controller itself made goes through the same threshold** — the
|
||||
operator's words: *too many changes means a human is actively working on it*, so one confirmation. The
|
||||
bound is the old brake's half, with three in place of one: one, two or three consumers of a larger
|
||||
provider go without a hand; two of two do not.
|
||||
|
||||
- **Approve** retires exactly the set waiting — the provider refuses any other set, so a person approves
|
||||
what they were shown.
|
||||
- **Reject** keeps the set active; the provider does not ask about it again while the set stays the same.
|
||||
The rejection is held by the provider's process: a restart asks again, loudly. A rejected set can still
|
||||
be approved later.
|
||||
- **Settled**: a waiting or rejected set ends without retiring when the mesh asks for any of it again or
|
||||
the set changes; a new stable set starts the count over.
|
||||
|
||||
**5. The backend remembers, not the process.** Each provider lists from its own backend what the mesh
|
||||
made, active and retired, with the mark. So a restart forgets nothing; a consumer the backend holds
|
||||
active and the mesh no longer asks for is retired by the same rules after a restart as before one; and a
|
||||
consumer **found disabled without the mark** — withdrawn before this record — is **adopted** as retired:
|
||||
marked, said, announced, its clock starting then. What the mesh made is known by the mark, or, before
|
||||
the mark existed, by its shape (postgres: a role valid until *infinity*, which only the
|
||||
mesh's role statement sets, owning a database of its own name; keycloak: the
|
||||
`mesh.provisioned` attribute it already had). Anything else is somebody else's and is never listed,
|
||||
retired or deleted.
|
||||
|
||||
**6. Cleanup is the controller's verbs, executed by the provider.**
|
||||
|
||||
- `cleanup list` — every retired consumer per provider: its age, its size where the backend can say,
|
||||
and why.
|
||||
- `cleanup delete <node> <provider-module> <consumer> --why` — one.
|
||||
- `cleanup delete --older-than <days> --why` — lists what it would delete; deletes only with the
|
||||
operator's explicit `--confirm`.
|
||||
- **The provider deletes**, because it owns its backend: the controller asks the provider's tool on that
|
||||
machine through the mesh, as any module's tool is asked, and never touches a backend itself. A provider
|
||||
refuses to delete anything active or asked for, and anything not retired.
|
||||
- **Every deletion is written to the hand-act log**, as is every approval and rejection — including one
|
||||
made by asking a provider's tool directly rather than through the controller.
|
||||
|
||||
**7. Every provider serves the same four tools**, the protocol between the verbs and the providers:
|
||||
`provisioner_retirement` (what is held, waiting, rejected and retired), `provisioner_retire_approve`,
|
||||
`provisioner_retire_reject` and `provisioner_delete`, each act requiring a why. And says one event,
|
||||
`provisioner.retirement`, whose `change` is `waiting`, `settled`, `approved`, `rejected`, `retired`,
|
||||
`reenabled`, `deleted` or `adopted`; the controller derives the permission to publish it for every module
|
||||
that receives contributions, as ADR 0224 does the standing events.
|
||||
|
||||
**8. Nothing is silent.** Retire, approve, reject, re-enable and delete are each announced and said in the
|
||||
provider's journal; waiting is an urgent condition and a rejection a warning one, both naming the
|
||||
provider and its machine; and a self-check probe raises a **low-severity `cleanup-waiting` condition**
|
||||
for any provider holding something retired **more than thirty days**.
|
||||
|
||||
**9. The withdrawal brake paragraph of ADR 0229 is replaced by this record.** The rest of 0229 — the
|
||||
lease, the epoch, the report's order, one writer at composition, the plan by compare-and-set, the lint,
|
||||
every consumed kind's contract — stands unchanged.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **The first rollout surfaces what is already there.** On the home server the consumer locked by an
|
||||
earlier withdrawal is adopted as retired; the control node's six set-aside databases are listed, and
|
||||
raise `cleanup-waiting` thirty days after their date; any consumer a backend holds open that the mesh no
|
||||
longer asks for becomes a retirement — waiting for a person if there are more than three. Nothing is
|
||||
deleted by the rollout.
|
||||
- **Retired data is still backed up and still takes space** until a person deletes it. That is the
|
||||
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 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.
|
||||
- **The rollout order**: the controller first (it derives the permission to publish the new event and
|
||||
hears it; an older controller makes the provider's announcement a refusal the provider logs), then the
|
||||
catalogue's two Go providers, then the SDK's release and each TypeScript provider as it is rebuilt.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| 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` |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0229](0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md)
|
||||
— replaced in part: its withdrawal brake paragraph. Everything else in it stands.
|
||||
- [ADR 0227](0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md) rule 4,
|
||||
*nothing dropped silently* — which this keeps: a stable result over the bound stops and raises a
|
||||
condition.
|
||||
- [ADR 0224](0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md)
|
||||
— the provider's events and their derived permission, which the retirement event follows.
|
||||
- [Issue 241](../04-ISSUES/241-one-unreadable-grants-file-dropped-every-database-on-the-control-node/00-report.md)
|
||||
— what withdrawal destroyed, and what it stopped destroying.
|
||||
- [To-be 45](../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md) §2, §4, §7 and Phase 2, and
|
||||
[the module protocol](../03-DESIGN/01-to-be/19-the-module-protocol.md), amended alongside.
|
||||
- mesh-catalog `modules/postgres` and `modules/keycloak` (`retirement.go`, `retire_pg.go`, `oidc.go`);
|
||||
mesh-sdk `src/provisioner` 0.1.12; mesh-controller's `retire` and `cleanup` verbs and its probe.
|
||||
@@ -201,6 +201,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0224** — [A provider that keeps failing a consumer is a problem the controller reports](0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md)
|
||||
- **0227** — [The core holds nine rules, each checked, and is built to them in six phases](0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md)
|
||||
- **0229** — [The core's order is a lease the store remembers, and an epoch a machine is sent once it reads one](0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md)
|
||||
- **0230** — [A consumer the mesh stops asking for is retired, and deleted only by a person](0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md)
|
||||
- **0231** — [A healer acts on what observation raised, and only observation says it worked](0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md)
|
||||
|
||||
### Its tiers, from the bottom up
|
||||
|
||||
Reference in New Issue
Block a user