railiance-platform/docs/placement-policy.md
codex dc4245361d
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Finish RPF-WP-0018; RPF-WP-0019 repository-complete
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>
2026-08-18 13:35:04 +02:00

261 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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: P0P4 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 P1P4 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 P1P2 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**.