Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,71 @@
|
||||
use super::*;
|
||||
use crate::database::tenant::{Manager, Pool};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn reaping_deadline_keeps_pending_and_active_pools_visible() {
|
||||
let directory = Directory::new();
|
||||
runtime()
|
||||
.run(async |scope| {
|
||||
let mut settings = settings(true, 2);
|
||||
settings.1.as_mut().unwrap().idle_timeout_ms = 86_400_000;
|
||||
let database = crate::database::Database::open(
|
||||
&directory.0,
|
||||
settings.0.remove("default").unwrap(),
|
||||
scope,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let manager = Manager::new(
|
||||
&directory.0,
|
||||
settings.1.unwrap(),
|
||||
std::slice::from_ref(&database),
|
||||
0,
|
||||
&[],
|
||||
&[],
|
||||
&["default"],
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
let mut cache = manager.cache.lock().await;
|
||||
let first_state = manager.reserve(&mut cache, (0, 1)).unwrap();
|
||||
let active = Arc::new(Pool {
|
||||
database: database.clone(),
|
||||
});
|
||||
{
|
||||
let mut ready = first_state.try_lock().unwrap();
|
||||
ready.pool = Some(active.clone());
|
||||
ready.ready = true;
|
||||
}
|
||||
let first = Arc::downgrade(&first_state);
|
||||
drop(first_state);
|
||||
manager.evict(&mut cache);
|
||||
assert_eq!(first.strong_count(), 1);
|
||||
|
||||
let pending_state = manager.reserve(&mut cache, (0, 2)).unwrap();
|
||||
let pending = Arc::downgrade(&pending_state);
|
||||
drop(pending_state);
|
||||
manager.evict(&mut cache);
|
||||
assert_eq!(pending.strong_count(), 0);
|
||||
assert_eq!(first.strong_count(), 1);
|
||||
|
||||
cache.entries.get_mut(&(0, 1)).unwrap().touched =
|
||||
Instant::now() - Duration::from_secs(172800);
|
||||
cache.check_at = Instant::now();
|
||||
manager.evict(&mut cache);
|
||||
assert_eq!(first.strong_count(), 1);
|
||||
drop(active);
|
||||
manager.evict(&mut cache);
|
||||
assert_eq!(first.strong_count(), 1);
|
||||
assert!(cache.check_at > Instant::now());
|
||||
cache.check_at = Instant::now();
|
||||
manager.evict(&mut cache);
|
||||
assert_eq!(first.strong_count(), 0);
|
||||
drop(cache);
|
||||
manager.close().await;
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tenant_configuration_rejects_unknown_controls_and_escaping_locations() {
|
||||
for tenant in [
|
||||
serde_json::json!({"database":"missing"}),
|
||||
serde_json::json!({"database":"default","mode":"table"}),
|
||||
serde_json::json!({"database":"default","max_pools":0}),
|
||||
serde_json::json!({"database":"default","idle_timeout_ms":0}),
|
||||
serde_json::Value::Null,
|
||||
] {
|
||||
let document = serde_json::json!({"database":{"default":{"type":"sqlite","path":"app.db","tenant_directory":"tenants"}}, "tenant":tenant});
|
||||
assert!(Settings::load(&Document::parse(document.to_string()).unwrap()).is_err());
|
||||
}
|
||||
for path in ["", "../tenants", "/tmp/tenants", "data/../tenants"] {
|
||||
let mut config = settings(true, 2);
|
||||
config.0.get_mut("default").unwrap().tenant_directory = Some(path.to_owned());
|
||||
assert!(config.0["default"].validate().is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlapping_tenant_namespaces_and_platform_files_are_rejected() {
|
||||
let directory = Directory::new();
|
||||
runtime()
|
||||
.run(async |scope| {
|
||||
let mut config = settings(true, 2);
|
||||
let mut secondary = config.0["default"].clone();
|
||||
secondary.path = "secondary.db".to_owned();
|
||||
secondary.tenant_directory = Some("tenants//".to_owned());
|
||||
config.0.insert("secondary".to_owned(), secondary);
|
||||
let mut models = bindings();
|
||||
models[0].database = Some("secondary");
|
||||
assert_eq!(
|
||||
Registry::prepare(&directory.0, config, &models, scope)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.kind,
|
||||
Kind::Connection
|
||||
);
|
||||
let mut config = settings(true, 2);
|
||||
config.0.get_mut("default").unwrap().path = "tenants/1.db".to_owned();
|
||||
assert_eq!(
|
||||
Registry::prepare(&directory.0, config, &bindings(), scope)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.kind,
|
||||
Kind::Connection
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_database_is_selected_explicitly_and_multiple_physical_sources_migrate() {
|
||||
let directory = Directory::new();
|
||||
runtime().run(async |scope| {
|
||||
let mut config = settings(true, 1);
|
||||
let mut security = config.0["default"].clone();
|
||||
security.path = "security.db".to_owned();
|
||||
security.tenant_directory = Some("security-tenants".to_owned());
|
||||
config.0.insert("security".to_owned(), security);
|
||||
config.1.as_mut().unwrap().database = "security".to_owned();
|
||||
let registry = Registry::prepare(&directory.0, config, &bindings(), scope).await.unwrap();
|
||||
registry.migrate_tenant(scope, 1).await.unwrap();
|
||||
let mut context = Context::new(registry.clone());
|
||||
context.select_tenant(scope, 1).await.unwrap();
|
||||
let marker = context.control(scope).await.unwrap().query(scope, "SELECT COUNT(*) FROM _dever_tenant_storage WHERE ready=1 AND length(fingerprint)=64", vec![], 1, |row| row.get::<_, i64>(0)).await.unwrap();
|
||||
assert_eq!(marker, [2]);
|
||||
assert!(registry.check_transactions(&[&[0,2]]).is_err());
|
||||
assert!(context.select_tenant(scope, 2).await.is_err());
|
||||
assert_eq!(context.tenant_id(), None);
|
||||
assert!(context.on(0, scope).await.is_err());
|
||||
registry.close().await;
|
||||
assert!(directory.0.join("security-tenants/1.db").exists());
|
||||
assert!(directory.0.join("tenants/1.db").exists());
|
||||
}).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn explicit_migration_routes_models_roles_and_control_to_distinct_physical_storage() {
|
||||
let directory = Directory::new();
|
||||
runtime()
|
||||
.run(async |scope| {
|
||||
let registry = Registry::prepare(&directory.0, settings(true, 4), &bindings(), scope)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(registry.tenant_enabled());
|
||||
assert!(registry.is_tenant(0));
|
||||
let mut missing = Context::new(registry.clone());
|
||||
assert_eq!(
|
||||
missing.on(0, scope).await.err().unwrap().kind,
|
||||
Kind::InvalidData
|
||||
);
|
||||
assert_eq!(
|
||||
missing.select_tenant(scope, 1).await.unwrap_err().kind,
|
||||
Kind::Migration
|
||||
);
|
||||
assert!(!directory.0.join("tenants").exists());
|
||||
let rows = missing
|
||||
.control(scope)
|
||||
.await
|
||||
.unwrap()
|
||||
.query(
|
||||
scope,
|
||||
"SELECT COUNT(*) FROM sqlite_schema WHERE name='items'",
|
||||
vec![],
|
||||
1,
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(rows, [0]);
|
||||
registry.migrate_tenant(scope, 1).await.unwrap();
|
||||
registry.migrate_tenant(scope, 2).await.unwrap();
|
||||
registry.migrate_tenant(scope, 1).await.unwrap();
|
||||
assert_eq!(
|
||||
registry.queue_tenants(0, scope).await.unwrap(),
|
||||
[Some(1), Some(2)]
|
||||
);
|
||||
assert_eq!(registry.queue_tenants(1, scope).await.unwrap(), [None]);
|
||||
for id in [1, 2] {
|
||||
let mut context = Context::new(registry.clone());
|
||||
context.select_tenant(scope, id).await.unwrap();
|
||||
assert_eq!(context.tenant_id(), Some(id));
|
||||
assert_eq!(context.detached().tenant_id(), Some(id));
|
||||
let owner = context.enter();
|
||||
context
|
||||
.on(0, scope)
|
||||
.await
|
||||
.unwrap()
|
||||
.execute(
|
||||
scope,
|
||||
"INSERT INTO items(id,value) VALUES(1,$1)",
|
||||
vec![crate::database::Parameter::Text(format!("tenant {id}"))],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.on(2, scope)
|
||||
.await
|
||||
.unwrap()
|
||||
.execute(scope, "INSERT INTO roles(id) VALUES(1)", vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
context.finish(scope, owner, true).await.unwrap();
|
||||
let rows = context
|
||||
.on(0, scope)
|
||||
.await
|
||||
.unwrap()
|
||||
.query(scope, "SELECT value FROM items", vec![], 1, |row| {
|
||||
row.get::<_, String>(0)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(rows, [format!("tenant {id}")]);
|
||||
let count = context
|
||||
.control(scope)
|
||||
.await
|
||||
.unwrap()
|
||||
.query(scope, "SELECT COUNT(*) FROM roles", vec![], 1, |row| {
|
||||
row.get::<_, i64>(0)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(count, [0]);
|
||||
}
|
||||
registry.close().await;
|
||||
assert!(directory.0.join("tenants/1.db").is_file());
|
||||
assert!(directory.0.join("tenants/2.db").is_file());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_switches_are_immediate_validated_and_do_not_hide_ready_queues() {
|
||||
let directory = Directory::new();
|
||||
runtime()
|
||||
.run(async |scope| {
|
||||
let registry = Registry::prepare(&directory.0, settings(true, 2), &bindings(), scope)
|
||||
.await
|
||||
.unwrap();
|
||||
registry.migrate_tenant(scope, 1).await.unwrap();
|
||||
let mut context = Context::new(registry.clone());
|
||||
context.select_tenant(scope, 1).await.unwrap();
|
||||
assert!(context.components_enabled(scope, &["shop"]).await.unwrap());
|
||||
registry
|
||||
.set_component(scope, 1, "shop", false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!context.components_enabled(scope, &["shop"]).await.unwrap());
|
||||
assert!(context.require_components(scope, &["shop"]).await.is_err());
|
||||
assert!(
|
||||
context
|
||||
.components_enabled(scope, &["platform"])
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(registry.queue_tenants(0, scope).await.unwrap(), [Some(1)]);
|
||||
for invalid in ["platform", "unknown", "dever"] {
|
||||
assert_eq!(
|
||||
registry
|
||||
.set_component(scope, 1, invalid, false)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.kind,
|
||||
Kind::InvalidData
|
||||
);
|
||||
}
|
||||
registry
|
||||
.set_component(scope, 1, "shop", true)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(context.components_enabled(scope, &["shop"]).await.unwrap());
|
||||
registry.close().await;
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_component_listing_paginates_and_observes_current_tenant_state() {
|
||||
let directory = Directory::new();
|
||||
runtime()
|
||||
.run(async |scope| {
|
||||
let mut models = bindings().to_vec();
|
||||
for component in ["billing", "catalog"] {
|
||||
models.push(crate::database::Binding {
|
||||
component,
|
||||
table: component,
|
||||
schema: if component == "billing" {
|
||||
Sql::same("CREATE TABLE billing(id BIGINT PRIMARY KEY)")
|
||||
} else {
|
||||
Sql::same("CREATE TABLE catalog(id BIGINT PRIMARY KEY)")
|
||||
},
|
||||
..bindings()[0].clone()
|
||||
});
|
||||
}
|
||||
let registry = Registry::prepare(&directory.0, settings(true, 2), &models, scope)
|
||||
.await
|
||||
.unwrap();
|
||||
registry.migrate_tenant(scope, 1).await.unwrap();
|
||||
registry.migrate_tenant(scope, 2).await.unwrap();
|
||||
let components = ["billing", "catalog", "shop", "platform", "unknown"]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let mut context = Context::new(registry.clone());
|
||||
assert!(
|
||||
context
|
||||
.disabled_components(scope, &components)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
context.select_tenant(scope, 1).await.unwrap();
|
||||
for component in ["billing", "catalog", "shop"] {
|
||||
registry
|
||||
.set_component(scope, 1, component, false)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
context
|
||||
.disabled_components(scope, &components)
|
||||
.await
|
||||
.unwrap(),
|
||||
["billing", "catalog", "shop"]
|
||||
.map(String::from)
|
||||
.into_iter()
|
||||
.collect()
|
||||
);
|
||||
registry
|
||||
.set_component(scope, 1, "catalog", true)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
context
|
||||
.disabled_components(scope, &components)
|
||||
.await
|
||||
.unwrap(),
|
||||
["billing", "shop"].map(String::from).into_iter().collect()
|
||||
);
|
||||
context.select_tenant(scope, 2).await.unwrap();
|
||||
assert!(
|
||||
context
|
||||
.disabled_components(scope, &components)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
registry.close().await;
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transactions_and_foreign_keys_cannot_cross_tenant_boundaries() {
|
||||
let directory = Directory::new();
|
||||
runtime()
|
||||
.run(async |scope| {
|
||||
let registry = Registry::prepare(&directory.0, settings(true, 2), &bindings(), scope)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(registry.check_transactions(&[&[0, 1]]).is_err());
|
||||
registry.check_transactions(&[&[0, 2]]).unwrap();
|
||||
registry.check_transactions(&[&[1, 2]]).unwrap();
|
||||
assert!(registry.check_transactions(&[&[2, 0, 1]]).is_err());
|
||||
registry.migrate_tenant(scope, 1).await.unwrap();
|
||||
let mut context = Context::new(registry.clone());
|
||||
context.select_tenant(scope, 1).await.unwrap();
|
||||
context
|
||||
.on(1, scope)
|
||||
.await
|
||||
.unwrap()
|
||||
.query(scope, "SELECT COUNT(*) FROM accounts", vec![], 1, |row| {
|
||||
row.get::<_, i64>(0)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let owner = context.enter();
|
||||
context
|
||||
.on(0, scope)
|
||||
.await
|
||||
.unwrap()
|
||||
.execute(
|
||||
scope,
|
||||
"INSERT INTO items(id,value) VALUES(1,'rollback')",
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(context.on(1, scope).await.is_err());
|
||||
assert!(context.select_tenant(scope, 2).await.is_err());
|
||||
context.finish(scope, owner, false).await.unwrap();
|
||||
assert_eq!(
|
||||
context
|
||||
.on(0, scope)
|
||||
.await
|
||||
.unwrap()
|
||||
.query(scope, "SELECT COUNT(*) FROM items", vec![], 1, |row| row
|
||||
.get::<_, i64>(0))
|
||||
.await
|
||||
.unwrap(),
|
||||
[0]
|
||||
);
|
||||
let owner = context.enter();
|
||||
context
|
||||
.on(1, scope)
|
||||
.await
|
||||
.unwrap()
|
||||
.execute(scope, "INSERT INTO accounts(id) VALUES(1)", vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(context.on(0, scope).await.is_err());
|
||||
context.finish(scope, owner, true).await.unwrap();
|
||||
assert_eq!(
|
||||
context
|
||||
.on(1, scope)
|
||||
.await
|
||||
.unwrap()
|
||||
.query(scope, "SELECT COUNT(*) FROM accounts", vec![], 1, |row| row
|
||||
.get::<_, i64>(0))
|
||||
.await
|
||||
.unwrap(),
|
||||
[1]
|
||||
);
|
||||
drop(context);
|
||||
registry.close().await;
|
||||
let mut invalid = bindings();
|
||||
invalid[0].references = &[1];
|
||||
assert_eq!(
|
||||
Registry::prepare(&directory.0, settings(true, 2), &invalid, scope)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.kind,
|
||||
Kind::Connection
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_tenant_configuration_preserves_ordinary_database_routing() {
|
||||
let directory = Directory::new();
|
||||
runtime()
|
||||
.run(async |scope| {
|
||||
let registry = Registry::prepare(&directory.0, settings(false, 2), &bindings(), scope)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!registry.tenant_enabled());
|
||||
registry.check_transactions(&[&[0, 1, 2]]).unwrap();
|
||||
let mut context = Context::new(registry.clone());
|
||||
assert!(context.select_tenant(scope, 1).await.is_err());
|
||||
context
|
||||
.on(0, scope)
|
||||
.await
|
||||
.unwrap()
|
||||
.execute(
|
||||
scope,
|
||||
"INSERT INTO items(id,value) VALUES(1,'platform')",
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(context.require_components(scope, &["shop"]).await.is_ok());
|
||||
drop(context);
|
||||
registry.close().await;
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bounded_cache_never_evicts_an_active_tenant_pool() {
|
||||
let directory = Directory::new();
|
||||
runtime()
|
||||
.run(async |scope| {
|
||||
let registry = Registry::prepare(&directory.0, settings(true, 1), &bindings(), scope)
|
||||
.await
|
||||
.unwrap();
|
||||
registry.migrate_tenant(scope, 1).await.unwrap();
|
||||
registry.migrate_tenant(scope, 2).await.unwrap();
|
||||
let mut first = Context::new(registry.clone());
|
||||
first.select_tenant(scope, 1).await.unwrap();
|
||||
first.on(0, scope).await.unwrap();
|
||||
let mut second = Context::new(registry.clone());
|
||||
assert_eq!(
|
||||
second.select_tenant(scope, 2).await.unwrap_err().kind,
|
||||
Kind::PoolExhausted
|
||||
);
|
||||
assert_eq!(
|
||||
registry.migrate_tenant(scope, 1).await.unwrap_err().kind,
|
||||
Kind::Pool
|
||||
);
|
||||
drop(first);
|
||||
second.select_tenant(scope, 2).await.unwrap();
|
||||
second.on(0, scope).await.unwrap();
|
||||
drop(second);
|
||||
registry.close().await;
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_or_missing_markers_and_files_never_fall_back_or_recreate_storage() {
|
||||
let directory = Directory::new();
|
||||
runtime()
|
||||
.run(async |scope| {
|
||||
let registry = Registry::prepare(&directory.0, settings(true, 1), &bindings(), scope)
|
||||
.await
|
||||
.unwrap();
|
||||
registry.migrate_tenant(scope, 1).await.unwrap();
|
||||
let mut context = Context::new(registry.clone());
|
||||
context.select_tenant(scope, 1).await.unwrap();
|
||||
context
|
||||
.on(0, scope)
|
||||
.await
|
||||
.unwrap()
|
||||
.execute(
|
||||
scope,
|
||||
"UPDATE _dever_tenant_ready SET fingerprint='wrong'",
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
drop(context);
|
||||
let mut next = Context::new(registry.clone());
|
||||
assert_eq!(
|
||||
next.select_tenant(scope, 1).await.unwrap_err().kind,
|
||||
Kind::Migration
|
||||
);
|
||||
registry.migrate_tenant(scope, 1).await.unwrap();
|
||||
next.control(scope)
|
||||
.await
|
||||
.unwrap()
|
||||
.execute(scope, "UPDATE _dever_tenant_storage SET ready=0", vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(next.select_tenant(scope, 1).await.is_err());
|
||||
assert!(registry.queue_tenants(0, scope).await.unwrap().is_empty());
|
||||
registry.migrate_tenant(scope, 1).await.unwrap();
|
||||
std::fs::remove_file(directory.0.join("tenants/1.db")).unwrap();
|
||||
assert!(next.select_tenant(scope, 1).await.is_err());
|
||||
assert!(!directory.0.join("tenants/1.db").exists());
|
||||
for id in [0, -1] {
|
||||
assert_eq!(
|
||||
registry.migrate_tenant(scope, id).await.unwrap_err().kind,
|
||||
Kind::InvalidData
|
||||
);
|
||||
}
|
||||
registry.close().await;
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_schema_migration_stays_unready_and_never_becomes_a_cached_success() {
|
||||
let directory = Directory::new();
|
||||
runtime().run(async |scope| {
|
||||
let registry = Registry::prepare(&directory.0, settings(true, 2), &bindings(), scope).await.unwrap();
|
||||
registry.migrate_tenant(scope, 1).await.unwrap();
|
||||
registry.close().await;
|
||||
drop(registry);
|
||||
let mut changed = bindings();
|
||||
changed[0].schema = Sql::same("CREATE TABLE items(id BIGINT PRIMARY KEY, value TEXT NOT NULL, changed BIGINT NOT NULL)");
|
||||
let registry = Registry::prepare(&directory.0, settings(true, 2), &changed, scope).await.unwrap();
|
||||
let mut context = Context::new(registry.clone());
|
||||
assert!(context.select_tenant(scope, 1).await.is_err());
|
||||
assert_eq!(registry.migrate_tenant(scope, 1).await.unwrap_err().kind, Kind::Migration);
|
||||
assert!(context.select_tenant(scope, 1).await.is_err());
|
||||
assert!(registry.queue_tenants(0, scope).await.unwrap().is_empty());
|
||||
registry.close().await;
|
||||
}).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
use crate::{
|
||||
adapter::Document,
|
||||
database::{Binding, Context, Kind, Registry, Settings, Sql, Storage},
|
||||
task::{Limits, Runtime},
|
||||
};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
mod cache;
|
||||
mod configuration;
|
||||
mod isolation;
|
||||
mod lifecycle;
|
||||
mod performance;
|
||||
#[cfg(feature = "postgres")]
|
||||
mod postgres;
|
||||
|
||||
static NEXT: AtomicU64 = AtomicU64::new(0);
|
||||
struct Directory(PathBuf);
|
||||
impl Directory {
|
||||
fn new() -> Self {
|
||||
let directory = Self(std::env::temp_dir().join(format!(
|
||||
"dever-tenant-{}-{}",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, Ordering::Relaxed)
|
||||
)));
|
||||
std::fs::create_dir(&directory.0).unwrap();
|
||||
directory
|
||||
}
|
||||
}
|
||||
impl Drop for Directory {
|
||||
fn drop(&mut self) {
|
||||
std::fs::remove_dir_all(&self.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime() -> Runtime {
|
||||
Runtime::new(Limits {
|
||||
threads: 2,
|
||||
workers: 2,
|
||||
tasks: 32,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn settings(tenant: bool, pools: usize) -> Settings {
|
||||
let mut document = serde_json::json!({"database":{"default":{"type":"sqlite","path":"platform.db","tenant_directory":"tenants","max_connections":2,"max_page_size":1,"wait_timeout_ms":100}}});
|
||||
if tenant {
|
||||
document["tenant"] =
|
||||
serde_json::json!({"database":"default","max_pools":pools,"idle_timeout_ms":10});
|
||||
}
|
||||
Settings::load(&Document::parse(document.to_string()).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn bindings() -> [Binding; 4] {
|
||||
[
|
||||
Binding {
|
||||
storage: Storage::Tenant,
|
||||
component: "shop",
|
||||
database: None,
|
||||
table: "items",
|
||||
schema: Sql::same("CREATE TABLE items(id BIGINT PRIMARY KEY, value TEXT NOT NULL)"),
|
||||
plan: None,
|
||||
references: &[],
|
||||
},
|
||||
Binding {
|
||||
storage: Storage::Global,
|
||||
component: "platform",
|
||||
database: None,
|
||||
table: "accounts",
|
||||
schema: Sql::same("CREATE TABLE accounts(id BIGINT PRIMARY KEY)"),
|
||||
plan: None,
|
||||
references: &[],
|
||||
},
|
||||
Binding {
|
||||
storage: Storage::Authorization,
|
||||
component: "dever",
|
||||
database: None,
|
||||
table: "roles",
|
||||
schema: Sql::same("CREATE TABLE roles(id BIGINT PRIMARY KEY)"),
|
||||
plan: None,
|
||||
references: &[],
|
||||
},
|
||||
Binding {
|
||||
storage: Storage::Control,
|
||||
component: "dever",
|
||||
database: None,
|
||||
table: "permissions",
|
||||
schema: Sql::same("CREATE TABLE permissions(id BIGINT PRIMARY KEY)"),
|
||||
plan: None,
|
||||
references: &[],
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use super::*;
|
||||
use crate::database::tenant::{Manager, Pool};
|
||||
use std::{hint::black_box, sync::Arc, time::Instant};
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit release performance probe"]
|
||||
fn cached_pool_reaping_cost() {
|
||||
println!("\ncase,sample,iterations,elapsed_ns,checksum");
|
||||
for count in [1, 32, 256] {
|
||||
for busy in [false, true] {
|
||||
let directory = Directory::new();
|
||||
runtime()
|
||||
.run(async |scope| {
|
||||
let mut settings = settings(true, count);
|
||||
settings.1.as_mut().unwrap().idle_timeout_ms = 86_400_000;
|
||||
let database = crate::database::Database::open(
|
||||
&directory.0,
|
||||
settings.0.remove("default").unwrap(),
|
||||
scope,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let manager = Manager::new(
|
||||
&directory.0,
|
||||
settings.1.unwrap(),
|
||||
std::slice::from_ref(&database),
|
||||
0,
|
||||
&[],
|
||||
&[],
|
||||
&["default"],
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
let mut cache = manager.cache.lock().await;
|
||||
let mut states = Vec::new();
|
||||
for tenant in 0..count {
|
||||
let state = manager.reserve(&mut cache, (0, tenant as i64 + 1)).unwrap();
|
||||
let mut ready = state.try_lock().unwrap();
|
||||
ready.pool = Some(Arc::new(Pool {
|
||||
database: database.clone(),
|
||||
}));
|
||||
ready.ready = true;
|
||||
states.push(Arc::downgrade(&state));
|
||||
}
|
||||
// 只测回收检查,不包含建库、SQL 或池获取;Weak 不改变使用中状态。
|
||||
let active = busy.then(|| states[0].upgrade().unwrap());
|
||||
for _ in 0..100 {
|
||||
manager.evict(&mut cache);
|
||||
}
|
||||
for sample in 0..15 {
|
||||
let started = Instant::now();
|
||||
for _ in 0..10_000 {
|
||||
black_box(&manager).evict(black_box(&mut cache));
|
||||
}
|
||||
let elapsed = started.elapsed().as_nanos();
|
||||
assert_eq!(states[0].strong_count(), if busy { 2 } else { 1 });
|
||||
println!("tenant_cache_{count}_{busy},{sample},10000,{elapsed},{count}");
|
||||
}
|
||||
drop(active);
|
||||
drop(cache);
|
||||
manager.close().await;
|
||||
assert!(states.iter().all(|state| state.strong_count() == 0));
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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();
|
||||
}
|
||||
Reference in new issue
Block a user