Lines
0 %
Functions
Branches
100 %
//! A throwaway Postgres for the lifetime of one command.
//!
//! Replaces the `sqlx-cli` shell-out: the database is created and migrated
//! in-process through `sqlx`, using the *same* `migrations/` directory the
//! server embeds via `server::db::MIGRATOR`. One set of migrations, one
//! migrator, no second implementation that can drift.
//! `initdb` and `pg_ctl` remain external programs because they are external
//! programs; there is no library form of them.
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result, bail};
use sqlx::migrate::Migrator;
/// The project's migrations, embedded at compile time from the same directory
/// `server::db::MIGRATOR` reads. Embedding needs no database, which is what
/// lets this binary be built before the server it goes on to test.
static MIGRATOR: Migrator = sqlx::migrate!("../migrations");
/// Ports tried in order. No allocator to lean on, and `pg_ctl -w` exits
/// non-zero when the server fails to come up, so a busy port is self-correcting.
const PORT_RANGE: std::ops::Range<u16> = 54320..54340;
/// Bounds on how long the server will let a client misbehave.
///
/// NOT a fix for the hang seen on 2026-08-28, and worth saying so: there the
/// server sat `idle / ClientRead` — it had answered and was waiting for the
/// client, so there was no statement to time out. These bound the failures they
/// can bound: a runaway query, and a transaction abandoned mid-flight holding
/// locks. Both otherwise sit until the job's own deadline kills it, an hour
/// later, with no indication of which test was responsible.
const TIMEOUTS: &[&str] = &[
"-c",
"statement_timeout=120s",
"idle_in_transaction_session_timeout=300s",
];
/// Durability is off on purpose. This cluster is initdb'd moments earlier and
/// dies with the container, so there is nothing for it to protect — every fsync
/// is pure cost, paid on cloud block storage, and 275 of the tests create and
/// migrate a database of their own. Measured worth ~2m30s of a test run.
const DURABILITY_OFF: &[&str] = &[
"fsync=off",
"synchronous_commit=off",
"full_page_writes=off",
"autovacuum=off",
/// A running cluster that stops itself when dropped.
pub struct Postgres {
data_dir: PathBuf,
port: u16,
/// Kept so the temp dir outlives the cluster.
_root: tempfile::TempDir,
}
impl Postgres {
pub fn database_url(&self) -> String {
format!("postgres://postgres@127.0.0.1:{}/nomisync", self.port)
impl Drop for Postgres {
fn drop(&mut self) {
// Best effort: the container is about to vanish anyway, but leaving a
// server holding a port breaks a second invocation in the same job.
let _ = Command::new("pg_ctl")
.args([
"-D",
&self.data_dir.to_string_lossy(),
"-m",
"immediate",
"stop",
])
.status();
fn run(program: &str, args: &[&str]) -> Result<()> {
let status = Command::new(program)
.args(args)
.status()
.with_context(|| format!("failed to run {program}"))?;
if !status.success() {
bail!("{program} exited with {status}");
Ok(())
/// initdb, start, create the database, migrate it.
pub async fn start() -> Result<Postgres> {
// initdb refuses to run as root, and under a uid with no passwd entry it
// dies inside getpwuid — the builder image supplies a named non-root user.
let root =
tempfile::TempDir::with_prefix("nomisync-ci-pg").context("could not make a temp dir")?;
let data_dir = root.path().join("pgdata");
let socket_dir = data_dir.join("sockets");
run(
"initdb",
&[
&data_dir.to_string_lossy(),
"-U",
"postgres",
"--auth=trust",
],
)
.context("initdb failed")?;
std::fs::create_dir_all(&socket_dir).context("could not make the socket dir")?;
let log = root.path().join("postgres.log");
let port = start_on_free_port(&data_dir, &socket_dir, &log)?;
let postgres = Postgres {
data_dir,
port,
_root: root,
};
println!(">> postgres: listening on 127.0.0.1:{port}");
create_and_migrate(&postgres).await?;
Ok(postgres)
fn start_on_free_port(data_dir: &Path, socket_dir: &Path, log: &Path) -> Result<u16> {
for port in PORT_RANGE {
// The socket directory lives inside PGDATA: a shared /tmp is not
// writable the same way in every container, and its lock file is where
// that first shows up.
let options = format!(
"-p {port} -c listen_addresses=127.0.0.1 -c unix_socket_directories={} {}",
socket_dir.display(),
[DURABILITY_OFF, TIMEOUTS].concat().join(" ")
);
let status = Command::new("pg_ctl")
"-l",
&log.to_string_lossy(),
"-w",
"-o",
&options,
"start",
.context("failed to run pg_ctl")?;
if status.success() {
return Ok(port);
bail!("no free port in {PORT_RANGE:?} accepted a postgres server")
async fn create_and_migrate(postgres: &Postgres) -> Result<()> {
use sqlx::{Connection, Executor, PgConnection};
let admin = format!("postgres://postgres@127.0.0.1:{}/postgres", postgres.port);
let mut conn = PgConnection::connect(&admin)
.await
.context("could not connect to the new cluster")?;
conn.execute("CREATE DATABASE nomisync")
.context("could not create the nomisync database")?;
conn.close().await.ok();
let mut conn = PgConnection::connect(&postgres.database_url())
.context("could not connect to the nomisync database")?;
MIGRATOR.run(&mut conn).await.context("migrations failed")?;
println!(
">> postgres: migrated ({} migrations)",
MIGRATOR.iter().count()