From a3c7e7e1f130985974e40344ce258c9acf2d3a35 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 26 Aug 2026 22:56:36 +0200 Subject: [PATCH] 011: providing is a facet, and the assignment is a third thing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Any hosted service can be a factory — an identity provider grants clients, an analytics service grants a tracking identity, a mail server grants mailboxes, an application platform grants a project that is several of those at once. Providing is a FACET a module may have, not a kind of module it is, which is the same conclusion this effort reached about services and applications arriving from the other direction. So `provider` stops being a category too. Two relational stores from different vendors both grant "a database" and are the sharpest possible test of the substitutability rule. They fail it completely — different protocol, dialect, driver, client library compiled into the consumer — so `database` stays a tag, now with two real providers rather than a thought experiment. The assignment is a third entity, recorded because the operator tried the alternative: modules were once node-agnostic and it did not survive. Several of a provider's properties belong to neither end — where its state lives, how it is reached, tuning derived from the machine's hardware, which instance serves a given consumer. Not the catalogue, because they differ per node; not the node, because they are about this module. A design with only modules and nodes has nowhere to put them, which is what node-agnostic ran out of. The current system already stores environment values per module AND per node, arriving the same way. Which answers the question asked directly: two nodes both run a store, so which serves a consumer? Neither obvious answer. Not the consumer naming a node — that is placement in the consumer's manifest, a game edited because a database moved. Not the consumer not caring — for presence it genuinely does not, for instantiation it cares permanently. What the consumer knows is the SCOPE of its own need: one instance shared across every instance of itself, or one each. That decides, and needs no node named. Then the mesh binds, and the binding is recorded on the assignment and is sticky — a resolver that re-derives which store serves a consumer will one day derive a different answer and relocate a database. --- .../011-the-module-graph/worked-provider.md | 122 ++++++++++++++++-- 1 file changed, 114 insertions(+), 8 deletions(-) diff --git a/01-RESEARCH/011-the-module-graph/worked-provider.md b/01-RESEARCH/011-the-module-graph/worked-provider.md index 450bc42..ac36f81 100644 --- a/01-RESEARCH/011-the-module-graph/worked-provider.md +++ b/01-RESEARCH/011-the-module-graph/worked-provider.md @@ -82,16 +82,48 @@ genuinely different relations. So: **two kinds of edge, one graph.** Instantiation implies presence. Presence does not imply instantiation. +## Which provider, when two nodes run one + +Asked directly, because two nodes can each run a relational store and a consumer has to be +served by one of them. Neither obvious answer is right. + +**Not "the consumer names the node."** That is placement in the consumer's manifest — a small +game edited because a database moved, which is the fault +[`proposal.md`](proposal.md) separates constraints from placement to avoid. + +**Not "the consumer does not care" either.** For presence it genuinely does not: a terminal is a +terminal. For instantiation it cares permanently, because the data lands in exactly one store +and the wrong choice is discovered long afterwards. + +**What the consumer does know is the scope of its own need.** Not which node — how many of the +thing it wants, relative to itself: + +| Scope | Means | Example | +|---|---|---| +| **shared** | one instance serves every instance of this consumer | the mesh's own registry: every node reads the same rows | +| **per instance** | each instance of this consumer gets its own | a local cache, a per-node queue | + +That is a property of the consumer, expressible without naming anything. And it is the thing +that actually decides: a shared need cannot be satisfied by a provider each node runs +separately, and a per-instance need should not be satisfied by a shared one. + +**Then the mesh binds, and the binding is recorded on the assignment.** Not recomputed. A +resolver that re-derives which store serves a consumer will one day derive a different answer and +relocate a database — so the binding is made once, written down, and changed only deliberately. + +The pieces that follow, none of them settled here: + +- **When several providers satisfy the scope**, something chooses — most plausibly locality, + preferring a provider on the same node. That is a default, and it must be overridable, because + the reason to override it is exactly the reason nobody anticipated it. +- **A binding is a thing that can be wrong.** Once recorded it can be inspected, and a consumer + bound to a store on a node that no longer exists is a question somebody can be asked rather + than a failure at connect time. +- **Moving a binding moves data.** Whatever the mechanism, changing it is a migration and not a + configuration change, and a design that lets it look like the latter will lose something. + ## What still has no answer -**Which postgres?** A mesh with two of them, and a game that wants a database. Presence would be -satisfied by either. Instantiation cannot be — the data will live in exactly one, and choosing -wrongly is not a preference, it is the game's data in the wrong place, discovered later. - -This is the *who chooses between providers* question from [`proposal.md`](proposal.md), and the -worked example shows it is far sharper for instantiation than for presence. For `terminal` the -consumer genuinely does not care. For a database it cares permanently. - **How many instances of postgres should exist?** One per mesh is wrong — a node that must work while disconnected cannot depend on a database elsewhere. One per node is wrong — the mesh's own registry is one thing, not one per node. So the answer is per-module, and nothing in the schema @@ -113,6 +145,44 @@ a declaration is composed *per node from what the node reported*, which is a str anything recorded so far. +## Providing is not a substrate thing + +The four pinned services are the obvious providers, and they are not a category. + +| Service | What a consumer asks it for | +|---|---| +| a relational store | a database, a user, credentials | +| another relational store, different vendor | a database — **and not the same one** | +| a message broker | a virtual host, a user, permissions | +| an object store | a bucket and keys | +| an image registry | a repository | +| an identity provider | a client, a realm, a secret | +| an analytics service | a site, and a tracking identity | +| a low-code data platform | a base, and a token | +| an application platform | a project, which is several of the above at once | +| a mail server | a mailbox, an alias, credentials | + +**Any hosted service can be a factory.** Providing is a facet a module may have, not a kind of +module it is — which is the same conclusion the effort reached about services and applications, +arriving from the other direction. + +That kills the last reason to keep *provider* as a category. A module runs something, or grants +something, or both, or neither. + +### Two stores, and why `database` still is not a name + +Two relational stores from different vendors both grant *a database*. They are the sharpest +possible test of the substitutability rule from [`proposal.md`](proposal.md), and they fail it +completely: different wire protocol, different dialect, different driver, different client +library compiled into the consumer. + +A consumer declaring `requires: database` and being handed either would break against one of +them. So the name promises what no provider delivers — and now with two real providers in the +catalogue rather than a thought experiment. + +*Database* remains a **tag**. It is how a person finds both. It is not how a consumer names what +it needs. + ## The same shape, three more times The message broker has all nine. So does the object store, and so does the image registry. They @@ -161,3 +231,39 @@ Revoking a virtual host drops whatever had not been delivered — not recoverabl The relation is the same and the blast radius is not, which is an argument for the provider deciding what revocation means rather than the mesh applying one rule to all of them. + + +## The assignment is a third thing + +Recorded because the operator tried the alternative and abandoned it: **modules were once +node-agnostic**, and it did not survive contact. + +The worked example says why. Several of a provider's nine properties are not properties of the +module at all: + +- **where its state lives** — a volume on a particular machine; +- **how it is reached** — the same module on two nodes may answer locally, on the network, or + publicly, and that is a per-assignment decision; +- **configuration derived from the hardware** — tuning follows the memory and storage of the + machine it landed on; +- **whether this instance is the one** a given consumer is provisioned from. + +None of those belong in the catalogue, because they differ per node. None belong in the node, +because they are about this module. **They belong to the pairing**, and a design with only +modules and nodes has nowhere to put them — which is what "node-agnostic" ran out of. + +So there are three entities, not two: + +> **a module** · **a node** · **an assignment**, which is a module on a node and carries its own +> configuration + +The current system already has this, arrived at the same way: environment values are stored per +module *and per node*, so a module's settings differ between the machines running it. + +**This does not put placement back in the manifest.** A module still says what must be true of a +node and never which node ([`proposal.md`](proposal.md)). What changes is that the *result* of +placing it is a thing with its own state, rather than a fact recorded on one of the two ends. + +And it makes the composed declaration question from above answerable: a declaration is built +from the module, the node's inventory, and the assignment between them. Three inputs, which is +why two were never enough.