PostgreSQL is a critical part of the SourceHut infrastructure in terms of availability, performance, and data integrity.
Goals
Prerequisites
Nice-to-have
If we have to restart the primary PostgreSQL daemon for any reason, we should take the opportunity to upgrade the host to Alpine 3.12.
First we'll have to prepare a warm standby (which can be promoted to primary at short notice, but does not accept queries), then we can reconfigure it as a hot standby (which will accept queries).
Set up a user on the primary for the standby to connect as for streaming replication. It must have the REPLICATION and LOGIN privileges and pg_hba.conf must be updated.
Take a base backup of the primary and ship it to the
standby. Be advised: we have full_page_writes disabled as a ZFS
optimization; this may cause performance degradation during the basebackup
process. pg_basebackup -PzFt -D basebackup. Place 'basedirectory' in
/var/lib/postgresql.
Nuke the postgres data directory's contents on the standby, su to postgres,
then run these commands: tar -C ~/12/data -xf ~/basebackup/base.tar.gz,
tar -C ~/12/data/pg_wal -xf ~/basebackup/pg_wal.tar.gz,
touch ~/12/data/standby.signal
Fill out primary_conninfo on the standby with the connection details for
the account created for replication. host=... port=5432 user=... password=...
Configure a replication slot on the primary for the standby:
SELECT * FROM pg_create_physical_replication_slot('konpaku');
Then set primary_slot_name to konpaku on the standby.
Set hot_standby to "on" on the standby.
Start the database, monitor logs for restore completion.
Sanity check the standby database.
Note: some of this procedure is adapted from the restore the base backup docs, but modified cause those docs are shit.
✓ Process tested
✓ Completed in production
pgbouncer will allow us to quickly change which database server the fleet is using at any given moment. It does not handle load balancing directly, we would need to use a secondary approach for this if desirable (DNS round robin and haproxy are suggested by the docs).
A configuration similar to the following will be deployed fleet-wide:
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 16432
pool_mode = transaction
auth_type = trust
auth_file = /etc/pgbouncer/userlist.txt
max_client_conn = 100
default_pool_size = 20
[databases]
xxx.sr.ht=host=remilia.sr.ht user=xxx.sr.ht password=...
config.ini's will point at localhost through pgbouncer rather than directly at
the database servers. The initd scripts will need to be updated to use pgbouncer so that services are started up correctly.
✓ Process tested
For web applications, we should abort requests which would write to the database and instead show a page explaining the circumstances. The user can attempt a refresh later to re-attempt the same action.
For the SSH pipelines:
For builds.sr.ht, if any database update is met with a read-only error, retry with an exponential backoff and block until it succeeds. If it exceeds a deadline, then abort the build.
For webhook processing celery workers, it should be acceptable for it to simply fail. Webhooks are only generated when the database is updated, so if the database can't be updated, ¯\_(ツ)_/¯!
There may be some room for future improvements in all of these areas, but this is sufficient for the MVP.
✓ core.sr.ht patch to (somewhat) gracefully handle a read-only database
✗ read-only support for celery jobs
✗ read-only support for git.sr.ht, hg.sr.ht SSH pipeline
✗ read-only support for ongoing builds.sr.ht jobs
If possible, this is to be announced in advance. It will cause a partial outage of all services.
First, prepare all services for failover by updating their pgbouncer configurations to point to the standby. Do not reload any of the pgbouncer instances until all service configs have been updated, then use a script to update them all at once.
#!/bin/sh
hosts=meta.sr.ht \
git.sr.ht \
hg.sr.ht \
builds.sr.ht \
lists.sr.ht \
todo.sr.ht \
man.sr.ht \
paste.sr.ht \
hub.sr.ht \
cirno1.sr.ht \
cirno2.sr.ht \
legacy.sr.ht \
mail-b.sr.ht
for host in $hosts
do
ssh $host doas service pgbouncer reload
done
✓ Process tested
pg_ctl promote on the standby✓ Process tested
In this procedure, the server being demoted is referred to as the "old master" and the new master is referred to as such.
Prerequisite: complete the standby promotion procedure
Create a replication slot for the old master:
SELECT * FROM pg_create_physical_replication_slot('name...');
Create a user for the old master with login and replication privs
Fill out primary_slot_name and primary_conninfo in the old master's
config, and set hot_standby to on
Confirm that the old master has replication access in the new master's
pg_hba.conf
Start the old master and monitor the logs, it should catch up
Sanity check the database
The old master is now the standby, and the old standby is now the master.
✓ Process tested
This procedure will incur up to a few minutes of downtime, but ideally the outage can only last as little as a few seconds.
At this point, the master has become the standby and is now up-to-date. Repeat the process again to upgrade the new standby and return the master/standby assignments to normal.
✗ TODO: Test this process
This process will incur a longer service degradation. The services will be in read-only mode for up to 30 minutes.
select pg_xlog_replay_pause()✗ TODO: Test this process
The easiest way to do this is likely to be mounting an old version of the ZFS dataset on either the master or standby server, and running a secondary PostgreSQL instance from it.
✗ TODO: Test this process
wal_level to replica; we are not going to use logical replication. TODO:
Will this create WAL files which are incompatible with one another?archive_command and set archive_mode to off; we're going to use
streaming replication instead of log shipping.It may be possible to always service SELECT statements from either the primary or standby. If we eventually require load balancing, these queries could be eligible. However, queries (or transactions?) which mutate the database may only be executed on the primary. Therefore:
We need to set up a test network with a primary and standby and induce failure scenarios to learn how our applications behave under those conditions and practice the failure resolution steps. Such a test network is already partially provisioned on Drew's home network.
This will require at least the following:
We should play out the following scenarios:
At each stage of these processes, observe application behavior and record these observations.
The whole setup needs to be performed on a secondary deployment and each service carefully tested under normal and failure mode conditions. Of particular interest is:
commit 98c1b5581450fb0e045b1ea44b84808352314b73 Author: Conrad Hoffmann <ch@bitfehler.net> Date: 2026-10-16T10:56:03+02:00 Fix pre-LLM-policy phrasing