Skip to content

postgres-multi-location 2.0.0 — deploy into an existing GVC - #513

Merged
jacobecox merged 2 commits into
mainfrom
claude/postgres-ml-gvc
Aug 28, 2026
Merged

postgres-multi-location 2.0.0 — deploy into an existing GVC#513
jacobecox merged 2 commits into
mainfrom
claude/postgres-ml-gvc

Conversation

@jacobecox

Copy link
Copy Markdown
Contributor

Review diff: git diff 73bec9a..HEAD -- postgres-multi-location/versions/2.0.0/
(73bec9a is the verbatim cp -R of 1.1.0; everything real is in the commit after it.)

Chain-critical: grafana-multi-location vendors this chart and cannot convert until it merges and publishes.

Why a major version

The chart no longer creates a GVC. The upgrade across that boundary is destructive: a chart that stops declaring a GVC makes helm upgrade prune it, taking every workload, volumeset and identity inside.

The gate holds. 1.1.0 was installed into its own chart-created GVC; the upgrade onto 2.0.0 with 1.1.0's real values file was refused at render, and the GVC, 3 workloads, 2 volumesets, 2 identities and 2 policies all still existed afterwards. Confirmed with both -f and --values. Note cpln helm upgrade exited 0 on that refusal — state was verified directly rather than from the exit code.

Also bumps the vendored subchart to etcd-multi-location 2.0.0, and renames global.gvc.locationsglobal.locations so one list still serves both charts.

The three-layer defence, each with both controls

Layer Result
minScale/maxScale: 0 + localOptions undeclared GVC location → replicas: 0, deactivated because maxScale is set to 0, while both declared locations reached 1/1
Boot-time GVC read 200 for its own GVC, 403 for two others — and 403 for its own once the scoped policy was deleted, back to 200 in 3 s when restored
Undeclared-location guard exitCode: 1 on a fresh volume, WARNING-and-keeps-serving on an initialised one

The middle row is the one worth noting: deleting the policy and watching the same call flip to 403 proves the grant is what makes it work, rather than the call succeeding for some unrelated reason.

The headline promise, proven properly

A genuinely hung leader — SIGSTOP on Patroni's worker and the postmaster, REST returning 000 — was replaced by automatic promotion in a surviving location in 43 s, with writes resuming through both proxy and pooler at +10 s and the old leader rejoining at streaming lag 0 with no split brain.

An earlier kill -9 restarted the container in ~12 s, under the 45 s TTL, so no failover occurred. That is recorded rather than discarded: it is exactly the "crash that looks like a failover test but is not one" trap, and only the hung-leader method actually exercises the promotion path.

This is also why the 3-location default stays: two locations cannot deliver automatic promotion, since the DCS loses quorum on either loss. Single-location is impossible here by construction — postgres-highly-available is the single-location template.

workload-list, with an independent control

Unlisted client times out on all three tiers; listed client exchanges SSLRequest bytes with all three and writes through the proxy; replication continues. Then switching to same-org restored the unlisted client's access — an independent control that the block really was the firewall and not something else.

Gates

  • Drift: two consecutive no-op upgrades, all 14 resources Unchanged, stored specs byte-identical.
  • render-vs-stored: 0 unexplained differences on two chart shapes (15 and 16 resources). R7 backfills were declared to match PR cockroach/pgedge/clickhouse/etcd-ml — make rendered match stored #512's measured set rather than copying a sibling's superset.
  • Backups end to end into a real bucket, both modes: logical (a correct 1687-byte dump containing all 5 rows; the cron fired only in backup.location) and wal-g (6 base backups exactly 900 s apart, 29 WAL segments, standbys logging Not the primary).

Known, pre-existing, deliberately not fixed

  • The wal-g restore cannot be followed as written. Steps 1 and 4 do work — backup-list lists all six and backup-fetch produced a valid 31 MB PGDATA with a matching system identifier. Steps 3 and 5 cannot: the volumeset name is {release}-postgres-vs with no values knob, and the workload hard-renders that URI at two call sites. No replacement was invented — an invented restore is a worse defect than the broken one.
  • A failed logical backup still uploads a 20-byte empty gzip, which looks like a backup in a bucket listing. Image-level, and the same defect pgedge's round found.
  • Any upgrade re-elects the leader, not only a deliberate primaryLocation change.
  • On a shared failure the message names etcd-multi-location, because the subchart renders first. The refusal and remedy are correct; Helm offers no way to reorder.

Testing

17 PASS · 0 FAIL · 1 PARTIAL (the pre-existing restore) · 0 BLOCKED. test-gvc-2 was never reshaped — both undeclared-location arms were driven from the values side — and its spec diffs identical against the recorded before-state.

Briefing updated in the same PR.

🤖 Generated with Claude Code

This chart no longer creates a GVC. Every resource lands in
`.Values.global.cpln.gvc`, `createsGvc: false`, and `templates/gvc.yaml` is
deleted. Third in the grafana stack's forced conversion order —
grafana-multi-location cannot convert until this publishes.

The three-layer defence, all three proven in the four conversions merged today:

1. A render-time `fail` on `hasKey .Values.global "gvc"`. 1.x carried the
   location list at `global.gvc.locations`, not a top-level `gvc`, so the guard
   matches that path. Verified against the real 1.1.0 values.yaml via -f and
   against --set global.gvc.name=..., --set global.gvc.locations[0].name=... and
   --set-json 'global.gvc={}'. The upgrade this prevents deletes the GVC and
   every workload, volumeset and identity in it in ~6s while printing
   `upgraded successfully`.

2. `defaultOptions.minScale/maxScale: 0` on all three long-running tiers, with
   `localOptions` supplying the real per-location counts. This matters more here
   than in etcd: a Patroni member in an undeclared location gets the FULL
   etcd3.hosts list, so it joins the DCS and can WIN the leader election, while
   the HAProxy backend list is rendered from the same values and does not
   contain it — every client loses the primary with all replicas `ready: true`.

3. A boot-time GVC read via a new `{release}-postgres-gvc-policy`, scoped
   `targetKind: gvc` + `targetLinks` to the one install GVC. Check A (values
   only, unconditional), Check B (etcd DCS quorum arithmetically impossible) and
   Check C (a declared location the GVC lacks); B and C hard-fail on a FRESH
   PGDATA and warn on an initialised one. The same read also warns when
   `primaryLocation` or `backup.location` names a location the GVC lacks — the
   backup cron has no script of its own to warn from.

   All nine rows measured by running the rendered script in the real image
   against a stub GVC API. The bound was tested against a blackhole (192.0.2.1,
   connect hangs), not NXDOMAIN: 31s including container start, vs a 40s worst
   case and a ~110s liveness budget. curl's --max-time is per attempt, so the
   retry is done in shell under `timeout 12`.

Also in this version:

- Vendored subchart bumped etcd-multi-location 1.0.2 -> 2.0.0 (the converted
  child). `global.gvc.locations` -> `global.locations`, which is the same rename
  this chart makes, so one edit serves both charts.
- `workload-list` self-inclusion, via a single `pg-ml.ownWorkloadLinks` helper
  used at every call site (cockroach 2.0.0's pattern). The list governs
  Patroni-to-Patroni replication, HAProxy's health checks, PgBouncer's pooling
  and the nightly dump; a list naming only clients cut the cluster off from
  itself in all four templates tested this batch.
- R7 backfills declared to match PR #512's measured set: inboundBlockedCIDR,
  outboundAllowHostname, outboundBlockedCIDR, inboundAllowWorkload and
  secret-volume recoveryPolicy.
- Both startup scripts derive the GVC from ${CPLN_GVC} rather than from Helm, so
  a hostname cannot drift from where the workload runs.
- README: Migrating from 1.x, converted Prerequisites/Architecture/Notes, and
  the logical-restore psql host corrected to the fully-qualified internal name
  (a bare short name does not resolve for a `standard` workload).
- briefings/postgres-multi-location.md updated against the shipped values.yaml.

The >=2 location minimum stays: the DCS is a stretched etcd cluster with one
member per location and cannot run in one. postgres-highly-available is the
single-location template.
@jacobecox
jacobecox merged commit 2fc5139 into main Aug 28, 2026
6 checks passed
@jacobecox
jacobecox deleted the claude/postgres-ml-gvc branch August 28, 2026 21:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant