Graduate issues 067 and 068 to decisions and to-be designs
ADR 0084 (extends 0027) — a provision is served by a node-scoped provider the consumer selects, defaulting to co-location; a module may instead carry a private embedded instance that is not a provision. Design: 01-to-be/23-choosing-a-provider. ADR 0085 (extends 0031) — a secret is a provision and the vault is the module that provides it; a module's own local secret becomes an ordinary pair credential that rotates through the existing machinery, while the controller's provisioning-credential mint (0048) is unchanged. Design: 01-to-be/24-the-secrets-vault; doc 13 amended to cross-link the non-pair secret. Issues 067/068 marked resolved with amended-design set. ADR index regenerated; records and index checks pass. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-09-20
|
||||
decisions:
|
||||
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
|
||||
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
|
||||
---
|
||||
|
||||
# 23 — Choosing a provider
|
||||
|
||||
A provision is named for what the consumer's code is coupled to
|
||||
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)):
|
||||
`postgres-database`, not `database`. That decides *what kind* of provider satisfies a requirement.
|
||||
It does not decide *which* provider, and the mesh runs more than one of most kinds.
|
||||
|
||||
## Why there is a choice at all
|
||||
|
||||
Node-specific services delivered to the mesh is the design, not an exception. Every control-capable
|
||||
node runs its own relational store, its own cache, its own object store; a single node may run
|
||||
several relational stores, each raised by the module that needed a particular engine or version.
|
||||
The one provision that is single today — the identity provider — already serves applications whose
|
||||
home is another node. So for a given provision name there are usually several providers, one per
|
||||
node, and they are **not** interchangeable: each holds different data and lives in a different
|
||||
place. A consumer bound to the wrong one reads the wrong database or takes a network hop it did not
|
||||
need.
|
||||
|
||||
Naming the kind is therefore only half of "how a consumer gets what it needs". The other half is
|
||||
which provider, and it has two shapes: **consume a provider**, or **carry your own**.
|
||||
|
||||
## Consuming a provider
|
||||
|
||||
A provider is not a mesh-wide singleton. It is identified by the node it runs on together with the
|
||||
module that provides it — a (node, module) pair. A consumer's requirement resolves to one such
|
||||
provider, and which one is part of the **assignment**, not the manifest
|
||||
([ADR 0046](../../02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md)):
|
||||
the same module, assigned twice, may be served by two different providers.
|
||||
|
||||
**The default is co-location.** A consumer that names no provider is served by the provider of that
|
||||
provision on its own node. This is the ordinary case and is meant to need nothing said — a module
|
||||
that wants a database wants, almost always, the database on the machine it runs on. A mesh that
|
||||
happens to run exactly one provider of a kind is simply the case where co-location and "the only
|
||||
one there is" name the same thing; that is *there happens to be one*, not a mesh-wide scope written
|
||||
into the provision.
|
||||
|
||||
**Coupling to data is named.** The exception to co-location is a consumer coupled to a *particular
|
||||
provider's contents*: two modules that must share one database, or a consumer that must reach a
|
||||
provider on a different node. That coupling is exactly what may not be guessed, so the assignment
|
||||
names the provider. Naming it is also what makes a later move safe — the mesh knows the binding is
|
||||
to that provider and not to whichever one is nearest.
|
||||
|
||||
**Ambiguity is refused, never resolved by picking.** If several providers of a kind exist, none is
|
||||
named, and none is co-located, the requirement is unsatisfiable and is refused with the candidates
|
||||
shown — the same stance
|
||||
[ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) took
|
||||
against a confidently-wrong match, applied to the instance rather than the dialect. A wrong answer
|
||||
delivered quietly costs more than a refusal.
|
||||
|
||||
## Carrying your own
|
||||
|
||||
A module need not consume a provider at all. It may carry its **own** instance of an engine inside
|
||||
its own composition — reachable only on the module's own network, publishing no host port, and
|
||||
**not** declared as a provision. Nothing else in the mesh can see it or bind to it, and it cannot
|
||||
collide with anything on a well-known port. To resolution it does not exist; it is an internal part
|
||||
of the module, like any other container the module runs.
|
||||
|
||||
This is legitimate but it is the exception, and the design says when: **only when a genuine engine
|
||||
fork or a pinned server version makes the shared provider unusable.** A module written against a
|
||||
customised engine, or one that needs an extension the node's provider does not carry, has no choice
|
||||
but to carry its own. A module that merely pins an old image of an ordinary engine does not — the
|
||||
version on a compose file is the *server's*, and the application talks to a newer shared server
|
||||
perfectly well once its data is migrated in. The rule is *share by default; embed only when a fork
|
||||
or a version forces it*. Most of the per-module stores that exist in the mesh being migrated onto
|
||||
are the first kind wearing the second's clothes, and consolidate onto the node's provider.
|
||||
|
||||
The distinction is worth stating because the two cases look identical from outside — a module with
|
||||
a database either way — and the mesh must be able to tell them apart to reason about either. A
|
||||
consumed provider is a binding the mesh records, rotates and can move. An embedded instance is a
|
||||
private detail the mesh does not manage and must not mistake for a provider.
|
||||
|
||||
## What is not settled here
|
||||
|
||||
A provider that moves between nodes must keep its identity, so that a consumer's recorded choice
|
||||
does not silently rebind to a different provider that inherited its place. That is a property the
|
||||
provider lifecycle must supply, and this document names it as a requirement rather than describing
|
||||
its mechanism.
|
||||
Reference in New Issue
Block a user