Files
core/test/rust/runtime/database/tenant/postgres.rs
T
2026-11-03 03:32:07 +00:00

101 lines
7.3 KiB
Rust

use super::*;
use crate::database::{Database, Lease, Options, Parameter};
use futures_util::FutureExt;
use std::panic::AssertUnwindSafe;
#[test]
#[ignore = "requires explicit isolated postgres_tenant_test template and CREATEDB"]
fn physical_databases_are_explicit_isolated_idempotent_and_fail_closed() {
let directory = Directory::new();
runtime().run(async |scope| {
let document = Document::load(&PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../config/setting.json")).unwrap();
let settings: serde_json::Value = document.decode().unwrap();
let admin: Options = serde_json::from_value(settings["database"]["postgres_test"].clone()).unwrap();
let mut options: Options = serde_json::from_value(settings["database"]["postgres_tenant_test"].clone()).unwrap();
let template = options.url.as_ref().unwrap().parse::<tokio_postgres::Config>().unwrap();
assert!(template.get_dbname().unwrap().contains("{case}"));
assert_eq!(options.url.as_ref().unwrap().matches("{case}").count(), 1);
let token = crate::database::scalar::generated_uuid().unwrap().to_string().replace('-', "");
options.url = Some(options.url.unwrap().replace("{case}", &token));
let control_name = options.url.as_ref().unwrap().parse::<tokio_postgres::Config>().unwrap().get_dbname().unwrap().to_owned();
assert!((1..=63).contains(&control_name.len()));
assert!(control_name.bytes().all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_'));
let prefix = format!("dv_{token}");
options.tenant_database_prefix = Some(prefix.clone());
options.max_connections = 2;
options.max_page_size = 1;
let tenant_options = options.clone();
let admin = Database::open(&directory.0, admin, scope).await.unwrap();
let mut admin_executor = admin.executor().await.unwrap();
let Lease::Postgres(lease) = &mut admin_executor.lease else { unreachable!() };
lease.call(async |client| client.batch_execute(&format!("CREATE DATABASE \"{control_name}\"")).await.map_err(Into::into)).await.unwrap();
let mut database = std::collections::BTreeMap::new();
database.insert("default".to_owned(), options);
let config = Settings(database, Some(crate::database::TenantOptions { database: "default".to_owned(), max_pools: 2, idle_timeout_ms: 100 }));
let mut registry = None;
let result = AssertUnwindSafe(async {
registry = Some(Registry::prepare(&directory.0, config, &bindings(), scope).await.unwrap());
let registry = registry.as_ref().unwrap();
let mut absent = Context::new(registry.clone());
assert_eq!(absent.select_tenant(scope, 1).await.unwrap_err().kind, Kind::Migration);
assert_eq!(admin_executor.query(scope, Sql::same("SELECT COUNT(*) FROM pg_database WHERE datname=$1"), vec![Parameter::Text(format!("{prefix}_1"))], 1, |row| row.get::<_, i64>(0)).await.unwrap(), [0]);
for id in [1, 2, 1] { registry.migrate_tenant(scope, id).await.unwrap(); }
assert_eq!(registry.queue_tenants(0, scope).await.unwrap(), [Some(1), Some(2)]);
for id in [1, 2] {
let mut context = Context::new(registry.clone());
context.select_tenant(scope, id).await.unwrap();
let owner = context.enter();
context.on(0, scope).await.unwrap().execute(scope, Sql::same("INSERT INTO items(id,value) VALUES(1,$1)"), vec![Parameter::Text(format!("tenant {id}"))]).await.unwrap();
context.on(2, scope).await.unwrap().execute(scope, "INSERT INTO roles(id) VALUES(1)", vec![]).await.unwrap();
assert!(context.on(1, scope).await.is_err());
context.finish(scope, owner, true).await.unwrap();
assert_eq!(context.on(0, scope).await.unwrap().query(scope, "SELECT value FROM items", vec![], 1, |row| row.get::<_, String>(0)).await.unwrap(), [format!("tenant {id}")]);
assert_eq!(context.control(scope).await.unwrap().query(scope, "SELECT COUNT(*) FROM roles", vec![], 1, |row| row.get::<_, i64>(0)).await.unwrap(), [0]);
}
// 冷租户的标记被真实表锁阻塞时,另一租户仍能进入;取消后可重新准备。
tokio::time::sleep(std::time::Duration::from_millis(110)).await;
Context::new(registry.clone()).select_tenant(scope, 1).await.unwrap();
let held = Database::open_existing(&directory.0, tenant_options.clone(), Some(&format!("{prefix}_2")), scope).await.unwrap();
let mut held_executor = held.executor().await.unwrap();
held_executor.begin(scope).await.unwrap();
held_executor.initialize(scope, "LOCK TABLE _dever_tenant_ready IN ACCESS EXCLUSIVE MODE").await.unwrap();
let copy = registry.clone();
let blocked = scope.spawn(move |child| async move { Context::new(copy).select_tenant(&child, 2).await }).unwrap();
let started = tokio::time::Instant::now();
loop {
let waiting = admin_executor.query(scope, Sql::same("SELECT COUNT(*) FROM pg_stat_activity WHERE datname=$1 AND wait_event_type='Lock'"), vec![Parameter::Text(format!("{prefix}_2"))], 1, |row| row.get::<_, i64>(0)).await.unwrap();
if waiting[0] > 0 { break; }
assert!(started.elapsed() < std::time::Duration::from_secs(2));
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
tokio::time::timeout(std::time::Duration::from_millis(500), Context::new(registry.clone()).select_tenant(scope, 1)).await.unwrap().unwrap();
assert!(blocked.stop().await.unwrap().is_none());
held_executor.finish(scope, true, false).await.unwrap();
drop(held_executor);
held.close();
Context::new(registry.clone()).select_tenant(scope, 2).await.unwrap();
registry.set_component(scope, 1, "shop", false).await.unwrap();
let mut context = Context::new(registry.clone());
context.select_tenant(scope, 1).await.unwrap();
assert!(!context.components_enabled(scope, &["shop"]).await.unwrap());
assert_eq!(registry.queue_tenants(0, scope).await.unwrap(), [Some(1), Some(2)]);
context.on(0, scope).await.unwrap().execute(scope, "UPDATE _dever_tenant_ready SET ready=0", vec![]).await.unwrap();
drop(context);
assert_eq!(Context::new(registry.clone()).select_tenant(scope, 1).await.unwrap_err().kind, Kind::Migration);
}).catch_unwind().await;
if let Some(registry) = registry.take() {
registry.close().await;
drop(registry);
}
// Pool::close 释放驱动句柄后,让同一 runtime 的连接任务完成退出。
tokio::time::sleep(std::time::Duration::from_millis(30)).await;
let Lease::Postgres(lease) = &mut admin_executor.lease else { unreachable!() };
for name in [format!("{prefix}_1"), format!("{prefix}_2"), control_name] {
lease.call(async |client| client.batch_execute(&format!("DROP DATABASE IF EXISTS \"{name}\"")).await.map_err(Into::into)).await.unwrap();
}
drop(admin_executor);
admin.close();
if let Err(panic) = result { std::panic::resume_unwind(panic); }
}).unwrap();
}