Skip to content

feat(database): add MySQL HA, Redis HA and MySQL PITR commands - #1142

Closed
paulocsanz wants to merge 1 commit into
masterfrom
pcs/mysql-redis-ha-mysql-pitr-cli
Closed

feat(database): add MySQL HA, Redis HA and MySQL PITR commands#1142
paulocsanz wants to merge 1 commit into
masterfrom
pcs/mysql-redis-ha-mysql-pitr-cli

Conversation

@paulocsanz

Copy link
Copy Markdown
Collaborator

Problem

railway postgres shipped HA, PITR and connection pooling for one engine, with that engine's specifics compiled into the CLI:

  • PATRONI_ENABLED was the definition of "this is a cluster"
  • WAL_ARCHIVE_BUCKET was the definition of "archiving is on"
  • hardcoded postgres-ssl / postgres-patroni predicates were the enable gate

MySQL HA, Redis HA and MySQL PITR are all in the product now, and none of them fit those constants. Redis clusters key off SENTINEL_ENABLED, MySQL off GR_ENABLED; MySQL's archive lives under BINLOG_ARCHIVE_*; neither has a coordinator tier.

What this does

Generalizes the tree rather than copying it three times. The subcommand bodies move to commands/database/ and take the engine as a parameter. railway postgres, railway mysql and railway redis are thin capability declarations wired to them, so --help only lists what an engine actually ships:

ha pitr pgbouncer
railway postgres
railway mysql ✅ standalone-only
railway redis

redis pitr isn't a runtime refusal — the subcommand doesn't exist.

Everything engine-specific is read, not compiled

The CLI no longer decides any of this; the template does, and it's read off the live config or the fetched template record:

  • haActiveVariable decides whether a root is a live cluster, so Sentinel and Group Replication are recognized without either being named.
  • haConversionConfig bounds convert. A role the companion doesn't declare doesn't exist in that topology, so --coordinators is refused for the colocated-coordinator engines instead of silently ignored, and each role's allowed counts come from the companion's own selectors. Read from the companion record — the same one the server-side gate reads. (A standalone service may carry a partial copy of it, or, like the redis template, none at all, so trusting the service's copy would skip the check for exactly the engine that needs it.)
  • adoptionImageEligibility replaces the hardcoded image guardrails. This fixes a real divergence: the old rules refused any minor-pinned image, which is every image mysql-ha/mysql publishes.
  • clusterWiring selects the switchover mechanism. Postgres keeps the Patroni client; Redis and MySQL use the declared per-node role/switchover endpoints, which is the only transport their nodes expose.

Adding an engine is a registry entry plus a command file; shipping a new supported major, or rewiring a cluster, stays a template update with no CLI release.

Two live-scaling bugs this surfaced

Both would have produced a broken cluster, and both are fixed here:

  1. peerHostsVariable was never stamped. A node added to a redis-ha or mysql-ha cluster booted knowing no peers. Now stamped on joining nodes only, per the platform contract — restamping existing nodes would mark the whole fleet stale for a change none of them needs to see.
  2. No quorum fence where the data nodes are the voters. Scaling to an even cluster was accepted and would fail its next failover. Now refused up front, never silently rounded.

Postgres is unchanged

Same subcommands, same flags, same JSON shapes, same ~/.railway/postgres-ops.jsonl trail (each engine now keeps its own, so an existing trail stays where its owner left it). The pgBackRest coverage probe is untouched — it's now selected by the engine's declared probe kind rather than skipped by name, so an engine without one reports nothing instead of a fake "unavailable".

Verification

  • cargo test — 1286 pass. New coverage for per-engine cluster/archive detection, the companion-declared role bounds, the peer-list stamping and quorum fence, the declared-HTTP probe contract, and the per-engine command surfaces.
  • cargo clippy / cargo fmt clean (no new warnings in any touched file).
  • Template declarations were read from the live API for all five templates (postgres-ha, redis-ha, mysql-ha, postgres-pitr, mysql-pitr) rather than assumed, and the fixtures in the tests mirror what those records actually contain.

Not covered here: convert on a bare-major Redis/MySQL image is refused (the cluster pins every node to the source's exact major.minor) with the retag as the remedy, but the CLI can't yet detect the running minor for you the way the dashboard does. Worth a follow-up.

`railway postgres` shipped HA, PITR and pooling for one engine, with the
engine's specifics compiled in: `PATRONI_ENABLED` as the definition of a
cluster, `WAL_ARCHIVE_BUCKET` as the definition of an archive, and hardcoded
Postgres image predicates as the enable gate. MySQL and Redis now have the
same managed features in the product, and none of them fit those constants.

Generalize the tree instead of copying it. The subcommand bodies move to
`commands/database/` and take the engine as a parameter; `railway postgres`,
`railway mysql` and `railway redis` are thin capability declarations wired to
them, so `--help` only ever lists what an engine actually ships (no PITR or
pooling under `redis`, no pooling under `mysql`).

What a cluster is, and how it is driven, now comes from what the template
declares rather than from the CLI:

  - `haActiveVariable` decides whether a root is a live cluster, so Sentinel
    and Group Replication are recognized without naming either.
  - `haConversionConfig` bounds `convert`: a role the companion does not
    declare does not exist in that topology, so `--coordinators` is refused
    for the colocated-coordinator engines instead of being ignored, and each
    role's counts come from the companion's own selectors. Read from the
    COMPANION record, which is what the server-side gate reads -- a standalone
    service may carry a partial copy or (redis) none at all.
  - `adoptionImageEligibility` replaces the hardcoded image guardrails. This
    fixes a real divergence: the old rules refused any minor-pinned image,
    which is every image mysql-ha/mysql publishes.
  - `clusterWiring` selects the switchover mechanism. Postgres keeps the
    Patroni client; Redis and MySQL use the declared per-node role/switchover
    endpoints, which is the only transport their nodes expose.

Two live-scaling gaps this surfaced, both of which would have produced a
broken cluster:

  - `peerHostsVariable` was never stamped, so a node added to a redis-ha or
    mysql-ha cluster booted knowing no peers. Now stamped on joining nodes
    only, per the platform contract -- restamping existing nodes would mark
    the whole fleet stale for a change none of them needs.
  - Nothing enforced the quorum shape where the data nodes are the voters, so
    scaling to an even cluster was accepted and would fail its next failover.

Postgres behaviour is unchanged: same subcommands, same flags, same JSON
shapes, same `~/.railway/postgres-ops.jsonl` trail (each engine now keeps its
own). The PITR live-coverage probe stays pgBackRest-only, selected by the
engine's declared probe kind rather than skipped by name, so an engine
without one reports nothing instead of a fake "unavailable"; MySQL PITR
refuses the HA workflow verbs up front, since its archiver only runs
standalone.
@paulocsanz paulocsanz added the release/minor Author minor release label Aug 27, 2026
@paulocsanz

Copy link
Copy Markdown
Collaborator Author

Superseded: split into two PRs so MySQL PITR lands on its own.

Closing this one.

@paulocsanz paulocsanz closed this Aug 27, 2026
@paulocsanz
paulocsanz deleted the pcs/mysql-redis-ha-mysql-pitr-cli branch August 27, 2026 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release/minor Author minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant