1 parent
cfc74d9db6
commit
dacdf9c27d
76 files changed
+5209
-741
No files matched your search
+124
-21
@@ -12,6 +12,7 @@ pub struct CachedFolder {
|
||||
pub unread_count: u32,
|
||||
pub uid_validity: u32,
|
||||
pub highest_modseq: u64,
|
||||
pub counter_generation: u64,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -68,6 +69,7 @@ pub fn get_all_folders(conn: &Connection) -> Result<Vec<CachedFolder>, String> {
|
||||
.query_map([], |row| {
|
||||
let is_subscribed_int: i32 = row.get(4)?;
|
||||
let highest_modseq_int: i64 = row.get(8)?;
|
||||
let counter_generation_int: i64 = row.get(9)?;
|
||||
Ok(CachedFolder {
|
||||
name: row.get(0)?,
|
||||
delimiter: row.get(1)?,
|
||||
@@ -78,6 +80,7 @@ pub fn get_all_folders(conn: &Connection) -> Result<Vec<CachedFolder>, String> {
|
||||
unread_count: row.get(6)?,
|
||||
uid_validity: row.get(7)?,
|
||||
highest_modseq: highest_modseq_int as u64,
|
||||
counter_generation: counter_generation_int as u64,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("Failed to query folders: {e}"))?;
|
||||
@@ -118,6 +121,7 @@ pub fn touch_all_folders(conn: &Connection) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn update_folder_status(
|
||||
conn: &Connection,
|
||||
name: &str,
|
||||
@@ -147,21 +151,6 @@ pub fn update_folder_sync_status(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn refresh_unread_count(conn: &Connection, folder_name: &str) -> Result<(), String> {
|
||||
let unread: u32 = conn
|
||||
.query_row(
|
||||
crate::dbq!(db_folders::Q_008),
|
||||
params![folder_name],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|e| format!("Failed to count unread messages: {e}"))?;
|
||||
|
||||
conn.execute(crate::dbq!(db_folders::Q_009), params![unread, folder_name])
|
||||
.map_err(|e| format!("Failed to update unread_count: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_unread_count(
|
||||
conn: &Connection,
|
||||
folder_name: &str,
|
||||
@@ -175,15 +164,74 @@ pub fn set_unread_count(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn mark_folder_messages_fresh(conn: &Connection, folder_name: &str) -> Result<(), String> {
|
||||
conn.execute(crate::dbq!(db_folders::Q_029), params![folder_name])
|
||||
.map_err(|e| format!("Failed to mark folder messages fresh: {e}"))?;
|
||||
pub fn set_folder_counts(
|
||||
conn: &Connection,
|
||||
folder_name: &str,
|
||||
total_count: u32,
|
||||
unread_count: u32,
|
||||
) -> Result<(), String> {
|
||||
conn.execute(
|
||||
crate::dbq!(db_folders::Q_030),
|
||||
params![total_count, unread_count, folder_name],
|
||||
)
|
||||
.map_err(|e| format!("Failed to set folder counts: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn adjust_unread_count(conn: &Connection, folder_name: &str, delta: i32) -> Result<(), String> {
|
||||
conn.execute(crate::dbq!(db_folders::Q_010), params![delta, folder_name])
|
||||
.map_err(|e| format!("Failed to adjust unread_count: {e}"))?;
|
||||
pub fn update_folder_sync_cursor(
|
||||
conn: &Connection,
|
||||
folder_name: &str,
|
||||
uid_validity: u32,
|
||||
highest_modseq: u64,
|
||||
) -> Result<(), String> {
|
||||
conn.execute(
|
||||
crate::dbq!(db_folders::Q_031),
|
||||
params![uid_validity, highest_modseq as i64, folder_name],
|
||||
)
|
||||
.map_err(|e| format!("Failed to update folder sync cursor: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_folder_counts_if_unchanged(
|
||||
conn: &Connection,
|
||||
folder_name: &str,
|
||||
expected_generation: u64,
|
||||
total_count: u32,
|
||||
unread_count: u32,
|
||||
) -> Result<bool, String> {
|
||||
let changed = conn
|
||||
.execute(
|
||||
crate::dbq!(db_folders::Q_032),
|
||||
params![
|
||||
total_count,
|
||||
unread_count,
|
||||
folder_name,
|
||||
expected_generation as i64
|
||||
],
|
||||
)
|
||||
.map_err(|e| format!("Failed to conditionally set folder counts: {e}"))?;
|
||||
Ok(changed > 0)
|
||||
}
|
||||
|
||||
pub fn pending_message_sync_count(conn: &Connection) -> Result<u32, String> {
|
||||
let mut pending = 0u64;
|
||||
for folder in get_all_folders(conn)? {
|
||||
if folder
|
||||
.flags
|
||||
.split(',')
|
||||
.any(|flag| flag.trim().eq_ignore_ascii_case("\\Noselect"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let cached = crate::db::messages::count_messages(conn, &folder.name)?;
|
||||
pending = pending.saturating_add(folder.total_count.saturating_sub(cached) as u64);
|
||||
}
|
||||
Ok(pending.min(u32::MAX as u64) as u32)
|
||||
}
|
||||
|
||||
pub fn mark_folder_messages_fresh(conn: &Connection, folder_name: &str) -> Result<(), String> {
|
||||
conn.execute(crate::dbq!(db_folders::Q_029), params![folder_name])
|
||||
.map_err(|e| format!("Failed to mark folder messages fresh: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -237,6 +285,7 @@ pub fn get_folder(conn: &Connection, name: &str) -> Result<Option<CachedFolder>,
|
||||
let result = conn.query_row(crate::dbq!(db_folders::Q_015), params![name], |row| {
|
||||
let is_subscribed_int: i32 = row.get(4)?;
|
||||
let highest_modseq_int: i64 = row.get(8)?;
|
||||
let counter_generation_int: i64 = row.get(9)?;
|
||||
Ok(CachedFolder {
|
||||
name: row.get(0)?,
|
||||
delimiter: row.get(1)?,
|
||||
@@ -247,6 +296,7 @@ pub fn get_folder(conn: &Connection, name: &str) -> Result<Option<CachedFolder>,
|
||||
unread_count: row.get(6)?,
|
||||
uid_validity: row.get(7)?,
|
||||
highest_modseq: highest_modseq_int as u64,
|
||||
counter_generation: counter_generation_int as u64,
|
||||
})
|
||||
});
|
||||
|
||||
@@ -433,6 +483,59 @@ mod tests {
|
||||
assert!(!is_folder_fresh(&conn, "Sent", 30).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_folder_counts_reject_stale_writer() {
|
||||
let conn = open_test_db();
|
||||
upsert_folder(&conn, "INBOX", None, None, "", true, 3444, 12, 1, 50).unwrap();
|
||||
|
||||
let generation = get_folder(&conn, "INBOX")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.counter_generation;
|
||||
assert!(set_folder_counts_if_unchanged(&conn, "INBOX", generation, 3444, 11).unwrap());
|
||||
assert!(!set_folder_counts_if_unchanged(&conn, "INBOX", generation, 3444, 10).unwrap());
|
||||
|
||||
let folder = get_folder(&conn, "INBOX").unwrap().unwrap();
|
||||
assert_eq!(folder.total_count, 3444);
|
||||
assert_eq!(folder.unread_count, 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counter_generation_blocks_aba_stale_writer() {
|
||||
let conn = open_test_db();
|
||||
upsert_folder(&conn, "INBOX", None, None, "", true, 3444, 12, 1, 50).unwrap();
|
||||
|
||||
let generation_12 = get_folder(&conn, "INBOX")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.counter_generation;
|
||||
assert!(set_folder_counts_if_unchanged(&conn, "INBOX", generation_12, 3444, 11).unwrap());
|
||||
|
||||
let generation_11 = get_folder(&conn, "INBOX")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.counter_generation;
|
||||
assert!(set_folder_counts_if_unchanged(&conn, "INBOX", generation_11, 3444, 12).unwrap());
|
||||
assert!(!set_folder_counts_if_unchanged(&conn, "INBOX", generation_12, 3444, 10).unwrap());
|
||||
let folder = get_folder(&conn, "INBOX").unwrap().unwrap();
|
||||
assert_eq!((folder.total_count, folder.unread_count), (3444, 12));
|
||||
assert!(folder.counter_generation > generation_12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_cursor_update_does_not_change_authoritative_counts() {
|
||||
let conn = open_test_db();
|
||||
upsert_folder(&conn, "INBOX", None, None, "", true, 3444, 12, 1, 50).unwrap();
|
||||
|
||||
update_folder_sync_cursor(&conn, "INBOX", 2, 75).unwrap();
|
||||
|
||||
let folder = get_folder(&conn, "INBOX").unwrap().unwrap();
|
||||
assert_eq!(folder.total_count, 3444);
|
||||
assert_eq!(folder.unread_count, 12);
|
||||
assert_eq!(folder.uid_validity, 2);
|
||||
assert_eq!(folder.highest_modseq, 75);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_stale_folders() {
|
||||
let conn = open_test_db();
|
||||
|
||||
@@ -476,6 +476,11 @@ pub fn get_unindexed_messages(conn: &Connection, limit: u32) -> Result<Vec<(Stri
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn count_unindexed_messages(conn: &Connection) -> Result<u32, String> {
|
||||
conn.query_row(crate::dbq!(db_messages::Q_015), [], |row| row.get(0))
|
||||
.map_err(|e| format!("Failed to count unindexed messages: {e}"))
|
||||
}
|
||||
|
||||
pub fn get_all_uids_and_flags(
|
||||
conn: &Connection,
|
||||
folder: &str,
|
||||
|
||||
+234
-15
@@ -7,6 +7,7 @@ use std::sync::{Arc, Mutex, OnceLock, mpsc};
|
||||
|
||||
use rusqlite::OptionalExtension as _;
|
||||
use rusqlite::types::{ToSql, ToSqlOutput, Value};
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{Executor as _, MySql, MySqlPool, PgPool, Postgres, Row as SqlxRow};
|
||||
@@ -20,18 +21,36 @@ const SQLITE_SCHEMA: &str = include_str!("../../migrations/sqlite/schema.sql");
|
||||
const MYSQL_SCHEMA: &str = include_str!("../../migrations/mysql/schema.sql");
|
||||
const PGSQL_SCHEMA: &str = include_str!("../../migrations/pgsql/schema.sql");
|
||||
|
||||
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[(
|
||||
47,
|
||||
include_str!("../../migrations/sqlite/V047__blob_keys_and_search_outbox.sql"),
|
||||
)];
|
||||
const MYSQL_MIGRATIONS: &[(u32, &str)] = &[(
|
||||
47,
|
||||
include_str!("../../migrations/mysql/V047__blob_keys_and_search_outbox.sql"),
|
||||
)];
|
||||
const PGSQL_MIGRATIONS: &[(u32, &str)] = &[(
|
||||
47,
|
||||
include_str!("../../migrations/pgsql/V047__blob_keys_and_search_outbox.sql"),
|
||||
)];
|
||||
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[
|
||||
(
|
||||
47,
|
||||
include_str!("../../migrations/sqlite/V047__blob_keys_and_search_outbox.sql"),
|
||||
),
|
||||
(
|
||||
48,
|
||||
include_str!("../../migrations/sqlite/V048__folder_counter_generation.sql"),
|
||||
),
|
||||
];
|
||||
const MYSQL_MIGRATIONS: &[(u32, &str)] = &[
|
||||
(
|
||||
47,
|
||||
include_str!("../../migrations/mysql/V047__blob_keys_and_search_outbox.sql"),
|
||||
),
|
||||
(
|
||||
48,
|
||||
include_str!("../../migrations/mysql/V048__folder_counter_generation.sql"),
|
||||
),
|
||||
];
|
||||
const PGSQL_MIGRATIONS: &[(u32, &str)] = &[
|
||||
(
|
||||
47,
|
||||
include_str!("../../migrations/pgsql/V047__blob_keys_and_search_outbox.sql"),
|
||||
),
|
||||
(
|
||||
48,
|
||||
include_str!("../../migrations/pgsql/V048__folder_counter_generation.sql"),
|
||||
),
|
||||
];
|
||||
|
||||
const TENANT_TABLES: &[&str] = &[
|
||||
"calendar_events",
|
||||
@@ -93,6 +112,26 @@ const IMPORT_TABLES: &[&str] = &[
|
||||
"user_meta",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DatabaseDiagnostics {
|
||||
pub backend: String,
|
||||
pub status: String,
|
||||
pub latency_ms: u128,
|
||||
pub pool_size: Option<u32>,
|
||||
pub idle_connections: Option<usize>,
|
||||
pub database_bytes: Option<u64>,
|
||||
pub engine_version: Option<String>,
|
||||
pub account_count: Option<u64>,
|
||||
pub schema_version_min: Option<u32>,
|
||||
pub schema_version_max: Option<u32>,
|
||||
pub schema_aligned: Option<bool>,
|
||||
pub expected_schema_version: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
type SqliteDiagnosticsProbe = (u64, Option<u32>, Option<u32>, Option<u64>, String);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DatabaseBackend {
|
||||
Sqlite,
|
||||
@@ -312,6 +351,188 @@ pub fn configured_backend() -> DatabaseBackend {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn diagnostics() -> DatabaseDiagnostics {
|
||||
let started = std::time::Instant::now();
|
||||
let expected = latest_schema_version();
|
||||
match DATABASE.get() {
|
||||
Some(ConfiguredDatabase::Sqlite { path }) => {
|
||||
let path = path.clone();
|
||||
let result = tokio::task::spawn_blocking(move || -> Result<SqliteDiagnosticsProbe, String> {
|
||||
let conn = rusqlite::Connection::open(&path)
|
||||
.map_err(|e| format!("failed to open SQLite diagnostics connection: {e}"))?;
|
||||
conn.query_row("SELECT 1", [], |_row| Ok(()))
|
||||
.map_err(|e| format!("SQLite probe failed: {e}"))?;
|
||||
let engine_version: String = conn
|
||||
.query_row("SELECT sqlite_version()", [], |row| row.get(0))
|
||||
.map_err(|e| format!("SQLite version probe failed: {e}"))?;
|
||||
let (count, min_version, max_version): (i64, Option<i64>, Option<i64>) = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*), MIN(schema_version), MAX(schema_version) FROM rustbox_accounts",
|
||||
[],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|
||||
)
|
||||
.map_err(|e| format!("SQLite account diagnostics failed: {e}"))?;
|
||||
let bytes = std::fs::metadata(&path).ok().map(|meta| meta.len());
|
||||
Ok((
|
||||
count.max(0) as u64,
|
||||
min_version.and_then(|value| u32::try_from(value).ok()),
|
||||
max_version.and_then(|value| u32::try_from(value).ok()),
|
||||
bytes,
|
||||
engine_version,
|
||||
))
|
||||
}).await;
|
||||
match result {
|
||||
Ok(Ok((count, min_version, max_version, bytes, engine_version))) => {
|
||||
DatabaseDiagnostics {
|
||||
backend: "sqlite".to_string(),
|
||||
status: "ok".to_string(),
|
||||
latency_ms: started.elapsed().as_millis(),
|
||||
pool_size: None,
|
||||
idle_connections: None,
|
||||
database_bytes: bytes,
|
||||
engine_version: Some(engine_version),
|
||||
account_count: Some(count),
|
||||
schema_version_min: min_version,
|
||||
schema_version_max: max_version,
|
||||
schema_aligned: Some(
|
||||
count == 0
|
||||
|| (min_version == Some(expected) && max_version == Some(expected)),
|
||||
),
|
||||
expected_schema_version: expected,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
other => DatabaseDiagnostics {
|
||||
backend: "sqlite".to_string(),
|
||||
status: "error".to_string(),
|
||||
latency_ms: started.elapsed().as_millis(),
|
||||
pool_size: None,
|
||||
idle_connections: None,
|
||||
database_bytes: None,
|
||||
engine_version: None,
|
||||
account_count: None,
|
||||
schema_version_min: None,
|
||||
schema_version_max: None,
|
||||
schema_aligned: None,
|
||||
expected_schema_version: expected,
|
||||
error: Some(match other {
|
||||
Ok(Err(error)) => error,
|
||||
Err(error) => format!("SQLite diagnostics task failed: {error}"),
|
||||
Ok(Ok(_)) => unreachable!(),
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
Some(ConfiguredDatabase::Mysql { pool, .. }) => {
|
||||
let result: Result<(i64, Option<i64>, Option<i64>, String), String> = async {
|
||||
sqlx::query_scalar::<_, i32>("SELECT 1")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| format!("MySQL probe failed: {e}"))?;
|
||||
let engine_version: String = sqlx::query_scalar("SELECT VERSION()")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| format!("MySQL version probe failed: {e}"))?;
|
||||
let (count, min, max): (i64, Option<i64>, Option<i64>) = sqlx::query_as("SELECT COUNT(*), MIN(schema_version), MAX(schema_version) FROM rustbox_accounts")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| format!("MySQL account diagnostics failed: {e}"))?;
|
||||
Ok((count, min, max, engine_version))
|
||||
}.await;
|
||||
let (status, account_count, min_version, max_version, engine_version, error) =
|
||||
match result {
|
||||
Ok((count, min, max, version)) => (
|
||||
"ok",
|
||||
Some(count.max(0) as u64),
|
||||
min.and_then(|v| u32::try_from(v).ok()),
|
||||
max.and_then(|v| u32::try_from(v).ok()),
|
||||
Some(version),
|
||||
None,
|
||||
),
|
||||
Err(error) => ("error", None, None, None, None, Some(error)),
|
||||
};
|
||||
DatabaseDiagnostics {
|
||||
backend: "mysql".to_string(),
|
||||
status: status.to_string(),
|
||||
latency_ms: started.elapsed().as_millis(),
|
||||
pool_size: Some(pool.size()),
|
||||
idle_connections: Some(pool.num_idle()),
|
||||
database_bytes: None,
|
||||
engine_version,
|
||||
account_count,
|
||||
schema_version_min: min_version,
|
||||
schema_version_max: max_version,
|
||||
schema_aligned: account_count.map(|count| {
|
||||
count == 0 || (min_version == Some(expected) && max_version == Some(expected))
|
||||
}),
|
||||
expected_schema_version: expected,
|
||||
error,
|
||||
}
|
||||
}
|
||||
Some(ConfiguredDatabase::Pgsql { pool, .. }) => {
|
||||
let result: Result<(i64, Option<i64>, Option<i64>, String), String> = async {
|
||||
sqlx::query_scalar::<_, i32>("SELECT 1")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| format!("PostgreSQL probe failed: {e}"))?;
|
||||
let engine_version: String = sqlx::query_scalar("SHOW server_version")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| format!("PostgreSQL version probe failed: {e}"))?;
|
||||
let (count, min, max): (i64, Option<i64>, Option<i64>) = sqlx::query_as("SELECT COUNT(*), MIN(schema_version), MAX(schema_version) FROM rustbox_accounts")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| format!("PostgreSQL account diagnostics failed: {e}"))?;
|
||||
Ok((count, min, max, engine_version))
|
||||
}.await;
|
||||
let (status, account_count, min_version, max_version, engine_version, error) =
|
||||
match result {
|
||||
Ok((count, min, max, version)) => (
|
||||
"ok",
|
||||
Some(count.max(0) as u64),
|
||||
min.and_then(|v| u32::try_from(v).ok()),
|
||||
max.and_then(|v| u32::try_from(v).ok()),
|
||||
Some(version),
|
||||
None,
|
||||
),
|
||||
Err(error) => ("error", None, None, None, None, Some(error)),
|
||||
};
|
||||
DatabaseDiagnostics {
|
||||
backend: "pgsql".to_string(),
|
||||
status: status.to_string(),
|
||||
latency_ms: started.elapsed().as_millis(),
|
||||
pool_size: Some(pool.size()),
|
||||
idle_connections: Some(pool.num_idle()),
|
||||
database_bytes: None,
|
||||
engine_version,
|
||||
account_count,
|
||||
schema_version_min: min_version,
|
||||
schema_version_max: max_version,
|
||||
schema_aligned: account_count.map(|count| {
|
||||
count == 0 || (min_version == Some(expected) && max_version == Some(expected))
|
||||
}),
|
||||
expected_schema_version: expected,
|
||||
error,
|
||||
}
|
||||
}
|
||||
None => DatabaseDiagnostics {
|
||||
backend: "unconfigured".to_string(),
|
||||
status: "error".to_string(),
|
||||
latency_ms: started.elapsed().as_millis(),
|
||||
pool_size: None,
|
||||
idle_connections: None,
|
||||
database_bytes: None,
|
||||
engine_version: None,
|
||||
account_count: None,
|
||||
schema_version_min: None,
|
||||
schema_version_max: None,
|
||||
schema_aligned: None,
|
||||
expected_schema_version: expected,
|
||||
error: Some("database is not configured".to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_account_user_hashes() -> Result<Vec<String>, String> {
|
||||
match DATABASE.get() {
|
||||
Some(ConfiguredDatabase::Sqlite { path }) => {
|
||||
@@ -1838,7 +2059,7 @@ fn tenant_prefix(user_hash: &str) -> Result<String, String> {
|
||||
Ok(format!("u_{}__", &normalized[..24]))
|
||||
}
|
||||
|
||||
fn latest_schema_version() -> u32 {
|
||||
pub fn latest_schema_version() -> u32 {
|
||||
let migrations = match configured_backend() {
|
||||
DatabaseBackend::Sqlite => SQLITE_MIGRATIONS,
|
||||
DatabaseBackend::Mysql => MYSQL_MIGRATIONS,
|
||||
@@ -2108,8 +2329,6 @@ fn ensure_server_tenant(
|
||||
let tenant = tenant_schema(PGSQL_SCHEMA, &prefix)?;
|
||||
let legacy_path = legacy_path.clone();
|
||||
runtime_wait(runtime, async move {
|
||||
// PostgreSQL advisory locks are session-scoped, so they work across all RustBox nodes
|
||||
// that share this database and disappear automatically with the connection.
|
||||
let lock_name = format!("rustbox:{prefix}");
|
||||
let mut lock_conn = pool.acquire().await.map_err(|e| {
|
||||
format!("failed to acquire PostgreSQL provisioning connection: {e}")
|
||||
|
||||
Reference in new issue
Block a user