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::().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::().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(); }