use std::sync::Arc;
use std::time::Duration;
use deadpool_postgres::Pool;
use parking_lot::Mutex;
use super::schema::bootstrap_ddl;
use super::PostgresBackend;
use crate::pg_common::{build_pool, err, pg_err, qualified_table, PgOpts, DEFAULT_SCHEMA};
use crate::Error;
impl PostgresBackend {
pub(crate) async fn new(config: &str, opts: PgOpts) -> Result<Self, Error> {
Self::new_with_pin_ttl(config, opts, DEFAULT_PIN_TTL).await
}
pub(crate) async fn new_with_pin_ttl(
config: &str,
opts: PgOpts,
pin_ttl: Duration,
) -> Result<Self, Error> {
let pool = build_pool(config, &opts)?;
let schema = if opts.schema.is_empty() {
DEFAULT_SCHEMA.to_owned()
} else {
opts.schema
};
{
let client = pool.get().await.map_err(err)?;
client
.batch_execute(&bootstrap_ddl(&schema))
.await
.map_err(err)?;
}
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,
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,
})
}
#[allow(dead_code)]
pub(crate) async fn system_identifier(&self) -> Result<u64, Error> {
Self::system_identifier_of(&self.pool).await
}
#[cfg(test)]
pub(crate) fn inject_fail_joint_commit(&mut self) {
self.fail_joint_commit = true;
}
#[cfg(test)]
pub(crate) fn clear_fail_joint_commit(&mut self) {
self.fail_joint_commit = false;
}
async fn system_identifier_of(pool: &Pool) -> Result<u64, Error> {
let client = pool.get().await.map_err(err)?;
let row = client
.query_one("SELECT system_identifier FROM pg_control_system()", &[])
.await
.map_err(pg_err)?;
let id: i64 = row.try_get(0).map_err(pg_err)?;
Ok(id as u64)
}
}
pub(crate) const DEFAULT_PIN_TTL_SECS: u64 = 60;
pub(super) const DEFAULT_PIN_TTL: Duration = Duration::from_secs(DEFAULT_PIN_TTL_SECS);
#[cfg(test)]
mod tests {
use super::*;
use crate::postgres::testing::{
fresh_schema, harness, harness_dsn, harness_with, shown_application_name,
shown_synchronous_commit, skip_note,
};
#[tokio::test]
async fn opens_with_synchronous_commit_on_by_default() {
let Some(backend) = harness("sync-default").await else {
return;
};
assert_eq!(
shown_synchronous_commit(&backend).await,
"on",
"the shipped tier is synchronous_commit = on"
);
}
#[tokio::test]
async fn synchronous_commit_knob_wires_per_session() {
let Some(backend) = harness_with(
"sync-off",
PgOpts {
synchronous_commit: false,
..Default::default()
},
)
.await
else {
return;
};
assert_eq!(
shown_synchronous_commit(&backend).await,
"off",
"the instance's knob wires onto pooled connections"
);
}
#[tokio::test]
async fn opens_with_the_default_crate_tag() {
let Some(backend) = harness("app-name-default").await else {
return;
};
assert_eq!(
shown_application_name(&backend).await,
"alkblobs",
"the default tag is the crate-identifying name"
);
}
#[tokio::test]
async fn application_name_tag_wires_per_session() {
let Some(backend) = harness_with(
"app-name-custom",
PgOpts {
application_name: "alkblobs-test-tag".to_owned(),
..Default::default()
},
)
.await
else {
return;
};
assert_eq!(
shown_application_name(&backend).await,
"alkblobs-test-tag",
"the instance's tag wires onto pooled connections"
);
}
#[tokio::test]
async fn constructor_rejects_zero_pool_size() {
let Some(dsn) = harness_dsn() else {
skip_note();
return;
};
let opened = PostgresBackend::new(
&dsn,
PgOpts {
schema: fresh_schema("zero-pool"),
max_size: 0,
..Default::default()
},
)
.await;
match opened {
Ok(_) => panic!("max_size 0 must fail at entry"),
Err(Error::Io(message)) => assert!(
message.contains("max_size must be positive"),
"typed entry validation: {message}"
),
Err(other) => panic!("max_size 0 must fail typed, got: {other}"),
}
}
}