mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 01:47:47 +08:00
fix: harden concurrency limits and high-RPM runtime paths
Bound request, stream, queue, and shutdown resource lifetimes. Reduce scheduler and Redis hot-path work and isolate database maintenance. Include regression coverage, load probes, and concurrency audit results.
This commit is contained in:
@@ -4,6 +4,70 @@ use sqlx::PgPool;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
const STATEMENT_TIMEOUT_ENV: &str = "AETHER_GATEWAY_DATA_POSTGRES_STATEMENT_TIMEOUT_MS";
|
||||
const LOCK_TIMEOUT_ENV: &str = "AETHER_GATEWAY_DATA_POSTGRES_LOCK_TIMEOUT_MS";
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct PostgresSessionTimeouts {
|
||||
statement_ms: u32,
|
||||
lock_ms: u32,
|
||||
}
|
||||
|
||||
impl PostgresSessionTimeouts {
|
||||
fn from_env() -> Result<Self, DataLayerError> {
|
||||
Ok(Self {
|
||||
statement_ms: read_timeout_env(STATEMENT_TIMEOUT_ENV, 30_000)?,
|
||||
lock_ms: read_timeout_env(LOCK_TIMEOUT_ENV, 3_000)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn apply(self, options: PgConnectOptions) -> PgConnectOptions {
|
||||
options.options([
|
||||
("statement_timeout", self.statement_ms),
|
||||
("lock_timeout", self.lock_ms),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_timeout_ms(name: &str, value: &str) -> Result<u32, DataLayerError> {
|
||||
value
|
||||
.trim()
|
||||
.parse::<u32>()
|
||||
.ok()
|
||||
.filter(|value| *value <= i32::MAX as u32)
|
||||
.ok_or_else(|| {
|
||||
DataLayerError::InvalidConfiguration(format!(
|
||||
"{name} must be milliseconds in 0..=2147483647 (0 disables the timeout)"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn read_timeout_env(name: &str, default: u32) -> Result<u32, DataLayerError> {
|
||||
match std::env::var(name) {
|
||||
Ok(value) => parse_timeout_ms(name, &value),
|
||||
Err(std::env::VarError::NotPresent) => Ok(default),
|
||||
Err(std::env::VarError::NotUnicode(_)) => Err(DataLayerError::InvalidConfiguration(
|
||||
format!("{name} must contain a valid integer"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Migration and historical backfill connections are discarded on every exit path,
|
||||
/// including cancellation, so their relaxed deadlines cannot escape into request work.
|
||||
pub async fn acquire_postgres_migration_connection(
|
||||
pool: &PgPool,
|
||||
) -> Result<sqlx::pool::PoolConnection<sqlx::Postgres>, sqlx::Error> {
|
||||
let mut conn = pool.acquire().await?;
|
||||
conn.close_on_drop();
|
||||
sqlx::query("SET statement_timeout = 0")
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
sqlx::query("SET lock_timeout = 0")
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
fn connect_options(config: &PostgresPoolConfig) -> Result<PgConnectOptions, DataLayerError> {
|
||||
config.validate()?;
|
||||
let options = PgConnectOptions::from_str(config.database_url.trim()).map_err(|err| {
|
||||
@@ -33,12 +97,16 @@ pub type PostgresPool = PgPool;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PostgresPoolFactory {
|
||||
config: PostgresPoolConfig,
|
||||
timeouts: PostgresSessionTimeouts,
|
||||
}
|
||||
|
||||
impl PostgresPoolFactory {
|
||||
pub fn new(config: PostgresPoolConfig) -> Result<Self, DataLayerError> {
|
||||
config.validate()?;
|
||||
Ok(Self { config })
|
||||
Ok(Self {
|
||||
config,
|
||||
timeouts: PostgresSessionTimeouts::from_env()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &PostgresPoolConfig {
|
||||
@@ -46,7 +114,7 @@ impl PostgresPoolFactory {
|
||||
}
|
||||
|
||||
pub fn connect_lazy(&self) -> Result<PostgresPool, DataLayerError> {
|
||||
let options = connect_options(&self.config)?;
|
||||
let options = self.timeouts.apply(connect_options(&self.config)?);
|
||||
Ok(PgPoolOptions::new()
|
||||
.min_connections(self.config.min_connections)
|
||||
.max_connections(self.config.max_connections)
|
||||
@@ -59,10 +127,171 @@ impl PostgresPoolFactory {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{connect_options, PostgresPoolFactory};
|
||||
use super::{connect_options, parse_timeout_ms, PostgresPoolFactory, PostgresSessionTimeouts};
|
||||
use crate::PostgresPoolConfig;
|
||||
use sqlx::postgres::PgSslMode;
|
||||
|
||||
#[tokio::test]
|
||||
async fn migration_connection_future_is_send() {
|
||||
fn assert_send(_: impl Send) {}
|
||||
|
||||
let pool = sqlx::postgres::PgPoolOptions::new()
|
||||
.connect_lazy("postgres://localhost/aether")
|
||||
.unwrap();
|
||||
assert_send(super::acquire_postgres_migration_connection(&pool));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_session_timeout_milliseconds() {
|
||||
assert_eq!(parse_timeout_ms("timeout", "0").unwrap(), 0);
|
||||
assert_eq!(parse_timeout_ms("timeout", " 3000 ").unwrap(), 3_000);
|
||||
assert_eq!(
|
||||
parse_timeout_ms("timeout", "2147483647").unwrap(),
|
||||
i32::MAX as u32
|
||||
);
|
||||
for invalid in ["", "-1", "3s", "2147483648", "4294967296"] {
|
||||
assert!(parse_timeout_ms("timeout", invalid).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_deadlines_preserve_unrelated_connection_options() {
|
||||
let options = PostgresSessionTimeouts {
|
||||
statement_ms: 30_000,
|
||||
lock_ms: 3_000,
|
||||
}
|
||||
.apply(sqlx::postgres::PgConnectOptions::new().options([("search_path", "audit")]));
|
||||
assert_eq!(
|
||||
options.get_options(),
|
||||
Some("-c search_path=audit -c statement_timeout=30000 -c lock_timeout=3000")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires an isolated AETHER_TEST_DATABASE_URL"]
|
||||
async fn live_session_deadlines_rollback_transactions_and_isolate_migration_overrides() {
|
||||
use crate::error::SqlxResultExt;
|
||||
use crate::{PostgresTransactionOptions, PostgresTransactionRunner};
|
||||
|
||||
let factory = PostgresPoolFactory {
|
||||
config: PostgresPoolConfig {
|
||||
database_url: std::env::var("AETHER_TEST_DATABASE_URL").expect("test database URL"),
|
||||
min_connections: 0,
|
||||
max_connections: 2,
|
||||
..PostgresPoolConfig::default()
|
||||
},
|
||||
timeouts: PostgresSessionTimeouts {
|
||||
statement_ms: 100,
|
||||
lock_ms: 40,
|
||||
},
|
||||
};
|
||||
let pool = factory.connect_lazy().unwrap();
|
||||
let table = format!("deadline_test_{}", uuid::Uuid::new_v4().simple());
|
||||
sqlx::query(&format!(
|
||||
"CREATE TABLE {table} (id INTEGER PRIMARY KEY, value INTEGER NOT NULL)"
|
||||
))
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query(&format!("INSERT INTO {table} VALUES (1, 0)"))
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut blocker = pool.begin().await.unwrap();
|
||||
sqlx::query(&format!("UPDATE {table} SET value = 7 WHERE id = 1"))
|
||||
.execute(&mut *blocker)
|
||||
.await
|
||||
.unwrap();
|
||||
let runner = PostgresTransactionRunner::new(pool.clone());
|
||||
let insert = format!("INSERT INTO {table} VALUES (2, 2)");
|
||||
let update = format!("UPDATE {table} SET value = 9 WHERE id = 1");
|
||||
let started = std::time::Instant::now();
|
||||
let error = runner
|
||||
.run_read_write(|tx| {
|
||||
Box::pin(async move {
|
||||
sqlx::query(&insert)
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
sqlx::query(&update)
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("SQLSTATE 55P03"), "{error}");
|
||||
assert!(started.elapsed() < std::time::Duration::from_secs(2));
|
||||
blocker.rollback().await.unwrap();
|
||||
assert_eq!(
|
||||
sqlx::query_scalar::<_, i64>(&format!("SELECT COUNT(*) FROM {table}"))
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
sqlx::query_scalar::<_, i32>(&format!("SELECT value FROM {table} WHERE id = 1"))
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
|
||||
let error = sqlx::query("SELECT pg_sleep(0.3)")
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_postgres_err()
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("SQLSTATE 57014"), "{error}");
|
||||
runner
|
||||
.run(
|
||||
PostgresTransactionOptions {
|
||||
statement_timeout_ms: Some(1_000),
|
||||
..PostgresTransactionOptions::read_write()
|
||||
},
|
||||
|tx| {
|
||||
Box::pin(async move {
|
||||
sqlx::query("SELECT pg_sleep(0.15)")
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
Ok(())
|
||||
})
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut migration = super::acquire_postgres_migration_connection(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query("SELECT pg_sleep(0.15)")
|
||||
.execute(&mut *migration)
|
||||
.await
|
||||
.unwrap();
|
||||
drop(migration);
|
||||
for _ in 0..2 {
|
||||
let configured: i64 = sqlx::query_scalar(
|
||||
"SELECT setting::BIGINT FROM pg_settings WHERE name = 'statement_timeout'",
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
configured, 100,
|
||||
"relaxed/local overrides must not leak into pooled requests"
|
||||
);
|
||||
}
|
||||
sqlx::query(&format!("DROP TABLE {table}"))
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool.close().await;
|
||||
}
|
||||
|
||||
fn ssl_mode(url: &str, require_ssl: bool) -> PgSslMode {
|
||||
connect_options(&PostgresPoolConfig {
|
||||
database_url: url.to_string(),
|
||||
|
||||
Reference in New Issue
Block a user