RPF-WP-0018 closed: all seven tasks done. The provider-declaration finding was adopted upstream and its canonical form is the provider: block in tenancy.yaml; adaptive-pricing declined the standing co-signature and supplied typed tier minima instead, recorded in ADR-0002. Three corrections against our own output are recorded in the documents rather than edited away. RPF-WP-0019 T03 done (ceiling of three, memory binding, apps-pg-2 named as overflow, enforced by make apps-pg-verify-capacity). T01/T02 are repository-complete: backup target, retention, per-consumer connection limits, role timeouts and Burstable resources are declared in source and published in s3-consumer-interfaces 1.1.0 before rollout. They stay in progress because no live application, backup success or restore proof exists, and declared configuration is not a section 13 artifact. T04 waits on that window. apps-pg R reason corrected to say the target is declared-not-applied rather than absent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
261 lines
14 KiB
Markdown
261 lines
14 KiB
Markdown
# S3 database placement policy
|
||
|
||
Work record: `RPF-WP-0018` T02, T03, T06
|
||
Effective: 2026-08-17
|
||
Framework: `net-kingdom/canon/standards/tenancy-posture_v0.1.md` §4.4, §8
|
||
Contract version of this document: `1.0.0`
|
||
Status: **provisional** — binding on this repo now; formally conditional on
|
||
the framework reaching `accepted`.
|
||
|
||
Answers `tenant-engine`'s 2026-08-16 request ("a default and the conditions
|
||
that override it, so migrations stop being decided by whichever neighbour was
|
||
consulted") and Tenancy Posture §19.2.
|
||
|
||
## 1. Ownership, and its limit
|
||
|
||
Draft-8 §8.2 assigns the placement rule to `railiance-platform`, substrate
|
||
numbers to the package repo, workload requirements to the consumer and tier
|
||
minimums to `adaptive-pricing`. **Accepted, with the scope stated:**
|
||
|
||
> **This repo owns the rule. The package repo owns the number.**
|
||
|
||
We decide *that* a cluster must publish a ceiling, what happens when it is
|
||
reached, what the default placement is, and who is accountable for each
|
||
workload's placement. We do not decide what `platform-pg`'s `max_connections`
|
||
is — that is `rapp-postgres`'s cluster CR, and `RAILIANCE-WP-0012` /
|
||
`RAILIANCE-WP-0015` moved that surface there deliberately.
|
||
|
||
This matters for §19.8, which asks *this repo* for `platform-pg`'s declared
|
||
maximum size. That question is misrouted by one hop. `rapp-postgres`'s own
|
||
analysis makes the case better than we could: memory probably binds before
|
||
connections do, at roughly 10MB per backend against a 1Gi limit, and that is
|
||
an observation only the operator of the package can make. Our obligation under
|
||
this policy is §4 below — that a ceiling and an overflow target **exist and
|
||
are published**. Theirs is what they are.
|
||
|
||
`adaptive-pricing` declined a standing co-signature on 2026-08-17 and supplied
|
||
the stronger replacement adopted by draft-8: typed tier minima are joined to
|
||
consumer and provider declarations at tier definition and on change. This repo
|
||
therefore owns this policy singly; it must validate, not copy, the commercial
|
||
constraint artifact.
|
||
|
||
## 2. The default, and what overrides it
|
||
|
||
The ladder is `net-kingdom` canon and is not restated here. Adopted by
|
||
reference: P0–P4 per Tenancy Posture §4.4.
|
||
|
||
**Default: P1 — database per consumer, shared cluster** — for platform
|
||
services. P3 for client-facing business apps, per
|
||
`business-app-service-contract` §1.2. Both already ratified as Decision 4.4.1;
|
||
this document adopts rather than re-decides them.
|
||
|
||
**P0 is not a sanctioned target.** It is the state repos arrive in. A consumer
|
||
sharing a database with another consumer is a defect with a migration owed,
|
||
not a placement.
|
||
|
||
**The distinction `tenant-engine` asked us to preserve, preserved:** sharing a
|
||
*cluster* is a capacity decision and is cheap to undo — create target, dump,
|
||
restore, swap the `secretKeyRef`, restart. Sharing a *database* entangles
|
||
schemas and makes relocation a schema merge. This policy says "shared" only
|
||
ever in the first sense. Any consumer on an S3-custody cluster owns a whole
|
||
database.
|
||
|
||
**Portability is an expectation, not a courtesy.** A consumer connects by
|
||
injected URL only; no cluster, namespace, host or database name in source.
|
||
`tenant-engine` adopted this unprompted and it is hereby the rule, because a
|
||
placement policy that cannot be executed is a preference.
|
||
|
||
## 3. Placement owner per workload
|
||
|
||
§19.2 asks for a recorded owner per workload so that "should this be dedicated"
|
||
has an answer that is not archaeology. The **placement owner** decides
|
||
placement changes for that workload and is accountable for watching its
|
||
triggers. It is not the same role as the operator of the cluster.
|
||
|
||
| Workload | Substrate | Provided level | Placement owner | Co-signer | §13 evidence |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| `audit-core` | `platform-pg` | P1 | `audit-core` | this repo | probes exist |
|
||
| `tenant-engine` | SQLite PVC current; `platform-pg` desired | off-ladder current; P1 desired | `tenant-engine` | this repo | source conformance; live cutover absent |
|
||
| `user-engine` | `user-engine-pg` | P2 | `net-kingdom` | this repo | not assessed here |
|
||
| `target-revenue` | `target-revenue-pg` | P2 | `target-revenue` | this repo | not assessed here |
|
||
| `forgejo` | `forgejo-db` | P2 | this repo | — | single consumer |
|
||
| `vergabe` | `apps-pg` | P1 | `vergabe-teilnahme` | this repo | **absent** |
|
||
| `coulomb_social` | `apps-pg` | P1 | `coulomb-social` | this repo | **absent** |
|
||
|
||
### 3.1 Two of these P1 levels are asserted without evidence
|
||
|
||
§13.1 claims a level only with its artifact present. The P1–P4 artifact is
|
||
*"provisioning declaration plus the platform's isolation probes"*.
|
||
|
||
`platform-pg` has them — `rapp-postgres` runs 19 adversarial probes against the
|
||
consumer boundary. **`apps-pg` has none.** `scripts/capture-apps-pg-evidence.py`
|
||
captures *capacity* evidence for `resource-control`; it makes no isolation
|
||
assertion. So the P1 levels recorded above for `vergabe` and `coulomb_social`
|
||
are, as of 2026-08-17, **provisioning declarations without probes** — stated
|
||
here rather than quietly carried, because a table of levels that mixes
|
||
evidenced and unevidenced entries without saying which is which is the failure
|
||
§13.1 exists to prevent.
|
||
|
||
Recorded as owed by this repo. Until the probes exist, those two rows should be
|
||
read as *intended* placement, not *verified* placement.
|
||
|
||
### 3.2 `apps-pg` cannot produce the noisy-neighbour artifact at all
|
||
|
||
Worth separating from the gap above, because it is not a matter of writing a
|
||
missing test. §13's P1–P2 noisy-neighbour artifact requires a per-consumer
|
||
usage baseline, a saturation run, and **evidence that the governance controls
|
||
bind — that the greedy consumer is held at its limits.**
|
||
|
||
`apps-pg` has no per-consumer governance controls. Its connection pool is not
|
||
partitioned, there is no per-consumer connection allowance, no statement
|
||
timeout and no resource limit on the pod. There is nothing that could be shown
|
||
to bind, so a run against it could only demonstrate that a saturating consumer
|
||
degrades its co-residents — which §13.4 explicitly rejects as an artifact that
|
||
"can only fail, or that passes by being run gently enough".
|
||
|
||
The honest sequence is therefore: **controls first, artifact second.** Writing
|
||
the probe before the limits exist would produce an overclaim wearing the
|
||
costume of evidence. Tracked as `RPF-WP-0019`.
|
||
|
||
**The consuming repo is the placement owner; this repo co-signs.** The
|
||
consumer knows its latency requirement, its erasure horizon and its compliance
|
||
position; we know the substrate. Neither can decide alone, and putting the
|
||
decision solely here would recreate the "ops repo holds a commercial decision"
|
||
problem §8.2 exists to avoid. A placement change needs both signatures.
|
||
|
||
New consumers record a placement owner at provisioning. A provisioning request
|
||
without one is incomplete.
|
||
|
||
## 4. Ceiling and overflow — the §19.8 obligation, generalised
|
||
|
||
**Rule P-4.1.** Every shared cluster under S3 policy MUST publish a **declared
|
||
maximum consumer count** and a **named overflow target**, both before its
|
||
third consumer is provisioned.
|
||
|
||
**Rule P-4.2.** The ceiling is stated as the *binding* constraint with its
|
||
number, not as a capacity boast. Where more than one resource could bind
|
||
first — connections versus memory is the live case — the declaration names
|
||
which is expected to bind and why. A ceiling derived from the resource that
|
||
fails *cleanly* while another fails *catastrophically* is a mis-stated ceiling:
|
||
connection exhaustion errors and is recoverable, memory pressure OOM-kills and
|
||
degrades every co-resident at once.
|
||
|
||
**Rule P-4.3.** The overflow target is a named substrate, not "we will build
|
||
one". "Provision a second cluster" is an answer; "revisit at the time" is not.
|
||
|
||
**Status against these rules today:**
|
||
|
||
| Cluster | Ceiling declared | Overflow target | Standing |
|
||
| --- | --- | --- | --- |
|
||
| `platform-pg` | 4 declarations (estimated; ADR-0004) | `platform-pg-2`, named but absent | 3/4 including the isolation probe; measure before the next workload |
|
||
| `apps-pg` | 3 consumers; 20 connections each, 1Gi memory binds first | `apps-pg-2` | 2/3; overflow must exist before a 4th |
|
||
| `forgejo-db` | n/a — single consumer | n/a | — |
|
||
|
||
Both live shared clusters are one consumer away from the trigger. This repo
|
||
owes `apps-pg`'s ceiling under its own rule, which is the correct asymmetry: a
|
||
policy whose author is exempt is not a policy.
|
||
|
||
## 5. Triggers, and who is watching them
|
||
|
||
§8.1: triggers MUST be **monitored**, not merely recorded. *"A trigger in a
|
||
YAML comment nobody re-reads is documentation, not control."*
|
||
|
||
This repo has no monitoring plane. `SCOPE.md` records that S3 emits to
|
||
`railiance-telemetry` "once the evidence plane exists — seeded 2026-08-11, not
|
||
yet implemented". Rather than claim coverage we do not have — the overclaim
|
||
§6 prohibits — each trigger below carries either a named watcher or the
|
||
literal marker `unmonitored`.
|
||
|
||
| # | Trigger (§8) | Signal that would fire it | Watcher | Cadence |
|
||
| --- | --- | --- | --- | --- |
|
||
| 1 | Noisy neighbour on a latency-critical path | Query latency regression on the consumer's own path | **consumer-observed** — the consumer notices, we do not | on incident |
|
||
| 2 | Compliance or residency requirement | Consumer declares it | placement owner | at declaration |
|
||
| 3 | Plan tier requiring a higher minimum | `adaptive-pricing` tier definition names a P floor | `adaptive-pricing` | on tier change |
|
||
| 4 | Erasure horizon no longer fits | Consumer's declared horizon < cluster retention floor | placement owner + this repo | at provisioning, then `unmonitored` |
|
||
| 5 | Connection or memory ceiling reached | Backend count vs declared ceiling; memory vs limit | **`unmonitored` — pending `railiance-telemetry`** | — |
|
||
|
||
**Trigger 5 is the one that matters and it is the one nobody is watching.**
|
||
That is the honest position. It is also why Rule P-4.1 forces the ceiling to be
|
||
declared *before the third consumer*: a threshold that is written down is at
|
||
least checkable by a human at provisioning time, which is strictly better than
|
||
a limit discovered by an OOM kill. Declaring the ceiling is a partial substitute
|
||
for monitoring it, and it is the part we can do this week.
|
||
|
||
**Closing trigger 5 belongs to `railiance-telemetry`, not to this workplan.**
|
||
Recorded here as a dependency so it is visible rather than assumed.
|
||
|
||
## 6. Service class co-residency — the §8.3.3 report
|
||
|
||
§8.3.2 declares service class as a category, never a priority. §8.3.3 requires
|
||
the platform to report which classes are co-resident, because "an unenforceable
|
||
risk that nobody can see is strictly worse than one that is stated".
|
||
|
||
| Cluster | Co-resident classes | Mismatch |
|
||
| --- | --- | --- |
|
||
| `platform-pg` | `latency-critical` (`tenant-engine`) + `batch` (`audit-core`) | **yes — reported** |
|
||
| `apps-pg` | `interactive` + `interactive` | no |
|
||
| `forgejo-db` | single consumer | n/a |
|
||
|
||
**`platform-pg` mixes `latency-critical` with `batch` today.** Under §8.3.1
|
||
co-residents are equal — community PostgreSQL has no resource governor, so
|
||
nothing prioritises `tenant-engine`'s synchronous authorization queries over
|
||
`audit-core`'s batch work. `tenant-engine` chose a 5s statement timeout
|
||
precisely because it sits on `flex-auth`'s synchronous path.
|
||
|
||
This is **reported, not remediated**. It may well still be the right call —
|
||
§8.3.2 says a recognised mismatch "may still be the right call, but it should
|
||
be a decision, not an accident of who was provisioned when". It is now a
|
||
decision. The escalation, if the mismatch bites, is P2 for `tenant-engine`, and
|
||
the known alternative — gateway-level prioritisation in a connection proxy — is
|
||
infrastructure this estate does not run and is not adopted here.
|
||
|
||
## 7. Retention floor and ceiling — the §19.9 answer
|
||
|
||
**Answered: yes to both, as a rule; the numbers belong to the package repo.**
|
||
|
||
**Rule R-7.1 — floor.** A consumer's requested `backupRetentionDays` below the
|
||
cluster's declared floor is a **validation error at provisioning**, not a quiet
|
||
disappointment. A consumer that asks for 1 day and silently gets 30 has been
|
||
told nothing true; a consumer that gets an error learns the actual constraint
|
||
at the only moment it can act on it.
|
||
|
||
**Rule R-7.2 — ceiling.** A maximum exists so no consumer can exhaust the
|
||
backup volume. Exceeding it is likewise an error, and the remedy named in the
|
||
error is P2 — a dedicated cluster whose retention is the consumer's own.
|
||
|
||
**Rule R-7.3 — the floor is a placement trigger, and this is the important
|
||
one.** Per `rapp-postgres` ADR-0002, physical backup is instance-wide, so on a
|
||
shared cluster **a consumer's erasure horizon is the maximum declared across
|
||
all co-residents**. A consumer needing a horizon shorter than that floor
|
||
**cannot have it at P1** and must move to P2 — for a reason that has nothing to
|
||
do with performance or capacity, which is exactly why it must be recorded
|
||
rather than assumed.
|
||
|
||
This makes someone else's retention choice everyone's, which is what
|
||
`tenant-engine` warned about on 2026-08-16 and was right to. Rule R-7.4
|
||
follows.
|
||
|
||
**Rule R-7.4 — inherited retention is disclosed, not inherited silently.** A
|
||
consumer joining a shared cluster is told its effective erasure horizon at
|
||
provisioning, and told again when a co-resident's change moves it. This is
|
||
§10.2 applied to retention and is implemented in
|
||
`docs/s3-consumer-interfaces.md` 1.1.0.
|
||
|
||
**Enforcement is the package repo's.** The validator lives where the CR lives.
|
||
This document states the rule; `rapp-postgres` and this repo's own `apps-pg`
|
||
onboarding enforce it.
|
||
|
||
## 8. What this policy deliberately does not do
|
||
|
||
- **It moves no workload.** Declaring where things are is separate from
|
||
changing where they are. Nothing above is a migration instruction.
|
||
- **It adds no QoS axis.** §19.12 resolved that: we could not enforce one.
|
||
- **It does not decide identity-provider placement** (realm-per-tenant versus
|
||
Organizations, §19.7). That is `key-cape`'s, on a different substrate.
|
||
- **It claims no monitoring.** See §5.
|
||
|
||
## Review interval
|
||
|
||
Six months, or on: a new shared-cluster consumer, a co-residency class change,
|
||
`railiance-telemetry` going live, or the framework reaching `accepted`. Next
|
||
review due **2027-02-17**.
|