101 lines
7.3 KiB
Rust
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();
|
|
}
|