use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use parking_lot::Mutex;
use tokio::time::Instant as TokioInstant;
use tokio_postgres::NoTls;
use super::test_support::SchemaJanitor;
use crate::backend::{Backend, ReadCursor, StagingCursor};
use crate::gc::{EngineWindowState, PinRow, WindowCandidate};
use crate::pg_common::{build_pool, err, pg_err, qualified_table, PgOpts};
use crate::postgres::pool::DEFAULT_PIN_TTL;
use crate::Error;
use super::PostgresBackend;
pub(crate) fn nanos_to_time(n: i64) -> SystemTime {
if n >= 0 {
UNIX_EPOCH + std::time::Duration::from_nanos(n as u64)
} else {
UNIX_EPOCH - std::time::Duration::from_nanos((-n) as u64)
}
}
pub(crate) const CONTENT: &[u8] = b"the quick brown fox jumps over the lazy dog";
pub(crate) struct Harness {
backend: PostgresBackend,
_janitor: SchemaJanitor,
}
impl Deref for Harness {
type Target = PostgresBackend;
fn deref(&self) -> &PostgresBackend {
&self.backend
}
}
impl DerefMut for Harness {
fn deref_mut(&mut self) -> &mut PostgresBackend {
&mut self.backend
}
}
pub(crate) static SCHEMA_SEQ: AtomicU64 = AtomicU64::new(0);
pub(crate) fn fresh_schema(tag: &str) -> String {
format!(
"abt_{}_{}_{}",
tag,
std::process::id(),
SCHEMA_SEQ.fetch_add(1, Ordering::Relaxed)
)
}
pub(crate) fn env_nonempty(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|v| !v.is_empty())
}
pub(crate) fn harness_dsn() -> Option<String> {
let host = env_nonempty("ALKBLOBS_PG_HOST")?;
Some(format!(
"host={host} port={} user={} password={} dbname={}",
env_nonempty("ALKBLOBS_PG_PORT").unwrap_or_else(|| "15432".to_owned()),
env_nonempty("ALKBLOBS_PG_USER").unwrap_or_else(|| "postgres".to_owned()),
env_nonempty("ALKBLOBS_PG_PASSWORD").unwrap_or_else(|| "poc".to_owned()),
env_nonempty("ALKBLOBS_PG_DB").unwrap_or_else(|| "blobs".to_owned()),
))
}
pub(crate) fn skip_note() {
eprintln!("skipping (no postgres harness server): set ALKBLOBS_PG_HOST/PORT/USER/PASSWORD/DB");
}
pub(crate) static LEASE_CANCEL_WINDOW: Mutex<Option<String>> = Mutex::new(None);
pub(crate) fn arm_lease_cancel_window(schema: &str) {
*LEASE_CANCEL_WINDOW.lock() = Some(schema.to_owned());
}
pub(crate) fn disarm_lease_cancel_window() {
*LEASE_CANCEL_WINDOW.lock() = None;
}
pub(crate) async fn lease_cancel_window_park(schema: &str) {
if LEASE_CANCEL_WINDOW.lock().as_deref() == Some(schema) {
std::future::pending::<()>().await;
}
}
pub(crate) async fn harness(tag: &str) -> Option<Harness> {
harness_with(tag, PgOpts::default()).await
}
pub(crate) async fn harness_with_ttl(tag: &str, pin_ttl: Duration) -> Option<Harness> {
let Some(dsn) = harness_dsn() else {
skip_note();
return None;
};
let schema = fresh_schema(tag);
let opts = PgOpts {
schema: schema.clone(),
..PgOpts::default()
};
let backend = PostgresBackend::new_with_pin_ttl(&dsn, opts, pin_ttl)
.await
.expect("open");
backend.ensure_gc_schema().await.expect("gc schema");
Some(Harness {
backend,
_janitor: SchemaJanitor::new(&dsn, &schema),
})
}
pub(crate) async fn harness_with(tag: &str, opts: PgOpts) -> Option<Harness> {
let Some(dsn) = harness_dsn() else {
skip_note();
return None;
};
let schema = fresh_schema(tag);
let opts = PgOpts {
schema: schema.clone(),
..opts
};
let backend = PostgresBackend::new(&dsn, opts).await.expect("open");
Some(Harness {
backend,
_janitor: SchemaJanitor::new(&dsn, &schema),
})
}
pub(crate) async fn harness_schemaless(tag: &str) -> Option<Harness> {
let Some(dsn) = harness_dsn() else {
skip_note();
return None;
};
let schema = fresh_schema(tag);
let backend = PostgresBackend::new_schemaless(&dsn, &schema)
.await
.expect("open");
Some(Harness {
backend,
_janitor: SchemaJanitor::new(&dsn, &schema),
})
}
pub(crate) async fn stored_value(backend: &PostgresBackend, key: &[u8]) -> Option<Vec<u8>> {
let cursor = backend.get(key).await.expect("get succeeds")?;
let (bytes, _) = cursor.read_range(0, cursor.len()).await.expect("read");
Some(bytes)
}
pub(crate) async fn staged(
backend: &PostgresBackend,
key: &[u8],
content: &[u8],
) -> Result<(), Error> {
let mut staging = backend.put_staged().await?;
staging.append(content).await?;
staging.commit(key).await
}
pub(crate) async fn shown_synchronous_commit(backend: &PostgresBackend) -> String {
let client = backend.pool.get().await.expect("checkout");
let messages = client
.simple_query("SHOW synchronous_commit")
.await
.expect("SHOW");
for message in &messages {
if let tokio_postgres::SimpleQueryMessage::Row(row) = message {
return row.get(0).expect("SHOW row").to_owned();
}
}
panic!("SHOW returned no row: {messages:?}")
}
pub(crate) async fn shown_application_name(backend: &PostgresBackend) -> String {
let client = backend.pool.get().await.expect("checkout");
let messages = client
.simple_query("SHOW application_name")
.await
.expect("SHOW");
for message in &messages {
if let tokio_postgres::SimpleQueryMessage::Row(row) = message {
return row.get(0).expect("SHOW row").to_owned();
}
}
panic!("SHOW returned no row: {messages:?}")
}
pub(crate) fn at(nanos: i64) -> SystemTime {
nanos_to_time(nanos)
}
pub(crate) async fn harness_gc(tag: &str) -> Option<Harness> {
let backend = harness(tag).await?;
backend.ensure_gc_schema().await.unwrap();
Some(backend)
}
pub(crate) fn stages(key: &[u8], visible_after: SystemTime) -> WindowCandidate {
WindowCandidate {
key: key.to_vec(),
visible_after,
}
}
pub(crate) fn pin_row(key: &[u8], owner: &[u8], expiry: SystemTime) -> PinRow {
PinRow {
key: key.to_vec(),
owner: owner.to_vec(),
expiry,
}
}
pub(crate) async fn window_rows(backend: &PostgresBackend) -> Vec<(Vec<u8>, i64)> {
let client = backend.pool.get().await.unwrap();
let sql = format!(
"SELECT key, visible_after FROM {} ORDER BY key",
backend.window_table
);
let rows = client.query(sql.as_str(), &[]).await.unwrap();
let mut out: Vec<(Vec<u8>, i64)> = rows
.iter()
.map(|row| (row.get::<_, Vec<u8>>(0), row.get::<_, i64>(1)))
.collect();
out.sort();
out
}
pub(crate) async fn pin_rows(backend: &PostgresBackend) -> Vec<(Vec<u8>, Vec<u8>, i64)> {
let client = backend.pool.get().await.unwrap();
let sql = format!(
"SELECT key, owner, expiry FROM {} ORDER BY key, owner",
backend.pins_table
);
let rows = client.query(sql.as_str(), &[]).await.unwrap();
rows.iter()
.map(|row| {
(
row.get::<_, Vec<u8>>(0),
row.get::<_, Vec<u8>>(1),
row.get::<_, i64>(2),
)
})
.collect()
}
pub(crate) async fn raw_connection(
dsn: &str,
) -> (tokio_postgres::Client, tokio::task::JoinHandle<()>) {
let (client, conn) = tokio_postgres::connect(dsn, NoTls)
.await
.expect("raw harness connect");
let handle = tokio::spawn(async move {
let _ = conn.await;
});
(client, handle)
}
pub(crate) fn arc_backend(backend: &PostgresBackend) -> Arc<PostgresBackend> {
Arc::new(PostgresBackend {
pool: backend.pool.clone(),
table: backend.table.clone(),
schema: backend.schema.clone(),
pins_table: backend.pins_table.clone(),
window_table: backend.window_table.clone(),
pin_ttl: backend.pin_ttl,
lease: Mutex::new(None),
liveness_sources: backend.liveness_sources.clone(),
#[cfg(test)]
fail_commit: backend.fail_commit,
#[cfg(test)]
fail_joint_commit: backend.fail_joint_commit,
#[cfg(test)]
fail_window_stage: backend.fail_window_stage,
})
}
pub(crate) async fn advisory_held(client: &tokio_postgres::Client, key: i64) -> bool {
let high = ((key as u64) >> 32) as u32;
let low = ((key as u64) & 0xFFFF_FFFF) as u32;
client
.query_one(
"SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND granted \
AND classid::text = $1 AND objid::text = $2",
&[&high.to_string(), &low.to_string()],
)
.await
.expect("advisory-lock probe")
.get::<_, i64>(0)
!= 0
}
pub(crate) async fn wait_for_advisory(
client: &tokio_postgres::Client,
key: i64,
want: bool,
what: &str,
) {
let deadline = TokioInstant::now() + std::time::Duration::from_secs(15);
loop {
if advisory_held(client, key).await == want {
return;
}
if TokioInstant::now() >= deadline {
panic!("{what}: the advisory lock never reached want = {want}");
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
}
pub(crate) async fn schema_tables(backend: &PostgresBackend) -> Vec<String> {
let client = backend.pool.get().await.unwrap();
let rows = client
.query(
"SELECT table_name::text FROM information_schema.tables \
WHERE table_schema = $1 ORDER BY table_name",
&[&backend.schema],
)
.await
.unwrap();
rows.iter().map(|row| row.get::<_, String>(0)).collect()
}
#[cfg(test)]
impl PostgresBackend {
#[cfg(test)]
pub(crate) async fn new_schemaless(config: &str, schema: &str) -> Result<Self, Error> {
let pool = build_pool(config, &PgOpts::default())?;
let schema = schema.to_owned();
let pins_table = qualified_table(&schema, "gc_pins");
let window_table = qualified_table(&schema, "gc_window");
Ok(Self {
pool,
table: qualified_table(&schema, "blobs"),
schema,
pins_table,
window_table,
pin_ttl: DEFAULT_PIN_TTL,
lease: Mutex::new(None),
liveness_sources: Arc::new(Mutex::new(Vec::new())),
#[cfg(test)]
fail_commit: false,
#[cfg(test)]
fail_joint_commit: false,
#[cfg(test)]
fail_window_stage: false,
})
}
}
impl PostgresBackend {
#[cfg(test)]
pub(crate) async fn table_exists(&self, table: &str) -> Result<bool, Error> {
let client = self.pool.get().await.map_err(err)?;
let row = client
.query_one(
"SELECT EXISTS (
SELECT 1 FROM pg_tables WHERE schemaname = $1 AND tablename = $2
)",
&[&self.schema, &table],
)
.await
.map_err(pg_err)?;
let present: bool = row.try_get(0).map_err(pg_err)?;
Ok(present)
}
}