use crate::config::types::TrackingTable;
use crate::migration_tracking::format_tracking_table_name;
use anyhow::{Context, Result};
use sqlx::postgres::PgConnectOptions;
use sqlx::{ConnectOptions, Connection, PgConnection};
use std::str::FromStr;
use std::time::Duration;
use tracing::log::LevelFilter;
pub fn advisory_lock_key(tracking_table: &TrackingTable) -> Result<i64> {
let name = format_tracking_table_name(tracking_table)?;
Ok(advisory_lock_key_from_name(&name))
}
fn advisory_lock_key_from_name(formatted_name: &str) -> i64 {
let digest = md5::compute(formatted_name.as_bytes());
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&digest.0[..8]);
i64::from_be_bytes(bytes)
}
pub struct MigrationLock {
conn: Option<PgConnection>,
key: i64,
}
impl MigrationLock {
pub async fn acquire(url: &str, tracking_table: &TrackingTable) -> Result<Self> {
let key = advisory_lock_key(tracking_table)?;
let mut conn = PgConnectOptions::from_str(url)
.context("Invalid database URL for the migration advisory lock")?
.log_slow_statements(LevelFilter::Off, Duration::from_secs(0))
.connect()
.await
.context("Failed to open dedicated connection for the migration advisory lock")?;
let acquired: bool = sqlx::query_scalar("SELECT pg_try_advisory_lock($1)")
.bind(key)
.fetch_one(&mut conn)
.await
.context("Failed to attempt the migration advisory lock")?;
if !acquired {
println!(
"Another pgmt operation is running against this database \
(advisory lock {} held); waiting...",
key
);
sqlx::query("SELECT pg_advisory_lock($1)")
.bind(key)
.execute(&mut conn)
.await
.context("Failed to acquire the migration advisory lock")?;
}
Ok(Self {
conn: Some(conn),
key,
})
}
pub async fn release(mut self) -> Result<()> {
if let Some(mut conn) = self.conn.take() {
if let Err(e) = sqlx::query("SELECT pg_advisory_unlock($1)")
.bind(self.key)
.execute(&mut conn)
.await
{
tracing::warn!(
"Failed to explicitly release migration advisory lock {} \
(will be released when the session ends): {}",
self.key,
e
);
}
let _ = conn.close().await;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tracking(schema: &str, name: &str) -> TrackingTable {
TrackingTable {
schema: schema.to_string(),
name: name.to_string(),
}
}
#[test]
fn test_default_tracking_table_key_is_stable() {
let key = advisory_lock_key(&tracking("public", "pgmt_migrations")).unwrap();
assert_eq!(key, 4779231646775632092);
}
#[test]
fn test_distinct_schema_or_name_yields_distinct_keys() {
let base = advisory_lock_key(&tracking("public", "pgmt_migrations")).unwrap();
let other_schema = advisory_lock_key(&tracking("app", "pgmt_migrations")).unwrap();
let other_name = advisory_lock_key(&tracking("public", "other_migrations")).unwrap();
assert_ne!(base, other_schema);
assert_ne!(base, other_name);
assert_ne!(other_schema, other_name);
}
#[test]
fn test_apply_and_provision_share_key_for_same_table() {
let a = advisory_lock_key(&tracking("public", "pgmt_migrations")).unwrap();
let b = advisory_lock_key(&tracking("public", "pgmt_migrations")).unwrap();
assert_eq!(a, b);
}
}