#![allow(deprecated)]
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use tensor_wasm_core::types::TenantId;
use tensor_wasm_tenant::{IsolationKind, TenantContext, TenantRegistry};
const TENANTS: u64 = 10;
const QUOTA_BYTES: u64 = 100 * 1024 * 1024; const KERNELS_PER_TENANT: u64 = 64;
const BYTES_PER_KERNEL: u64 = 256 * 1024;
fn make_ctx(id: u64) -> TenantContext {
TenantContext::builder(TenantId(id))
.with_isolation(IsolationKind::StreamIsolated)
.with_stream_id(id)
.with_memory_quota_bytes(QUOTA_BYTES)
.build()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn ten_tenants_concurrent_kernels_have_no_contamination() {
let (reg, cap) = TenantRegistry::new();
for i in 0..TENANTS {
reg.register(make_ctx(i)).expect("register");
}
let launched: Vec<Arc<AtomicU64>> = (0..TENANTS).map(|_| Arc::new(AtomicU64::new(0))).collect();
let per_tenant: Vec<Arc<_>> = (0..TENANTS)
.map(|i| reg.get(TenantId(i), &cap).expect("ctx"))
.collect();
let mut handles = Vec::new();
for tenant_id in 0..TENANTS {
let ctx = Arc::clone(&per_tenant[tenant_id as usize]);
let launched = Arc::clone(&launched[tenant_id as usize]);
handles.push(tokio::spawn(async move {
for _ in 0..KERNELS_PER_TENANT {
ctx.consume_bytes(BYTES_PER_KERNEL).expect("consume");
launched.fetch_add(1, Ordering::Relaxed);
tokio::task::yield_now().await;
ctx.release_bytes(BYTES_PER_KERNEL);
}
}));
}
for h in handles {
h.await.expect("task");
}
for (i, count) in launched.iter().enumerate() {
assert_eq!(
count.load(Ordering::Relaxed),
KERNELS_PER_TENANT,
"tenant {i} kernel count",
);
}
for i in 0..TENANTS {
let ctx = reg.get(TenantId(i), &cap).expect("ctx");
assert_eq!(
ctx.bytes_in_use(),
0,
"tenant {i} should have released all bytes; cross-tenant contamination?",
);
assert_eq!(ctx.quota(), QUOTA_BYTES, "tenant {i} quota");
}
assert_eq!(reg.len(&cap) as u64, TENANTS);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_overrun_is_contained_to_one_tenant() {
let (reg, cap) = TenantRegistry::new();
for i in 0..TENANTS {
reg.register(make_ctx(i)).expect("register");
}
let offender = 3u64;
let offender_ctx = reg.get(TenantId(offender), &cap).unwrap();
let offender_handle = tokio::spawn(async move {
offender_ctx.consume_bytes(QUOTA_BYTES).unwrap();
let err = offender_ctx.consume_bytes(1).unwrap_err();
assert!(format!("{err}").contains("memory exhausted"));
});
let other_ctxs: Vec<Arc<_>> = (0..TENANTS)
.filter(|i| *i != offender)
.map(|i| reg.get(TenantId(i), &cap).expect("ctx"))
.collect();
let mut other_handles = Vec::new();
for ctx in other_ctxs {
other_handles.push(tokio::spawn(async move {
for _ in 0..KERNELS_PER_TENANT {
ctx.consume_bytes(BYTES_PER_KERNEL).expect("consume");
tokio::task::yield_now().await;
ctx.release_bytes(BYTES_PER_KERNEL);
}
assert_eq!(ctx.bytes_in_use(), 0);
}));
}
offender_handle.await.expect("offender");
for h in other_handles {
h.await.expect("other");
}
let off = reg.get(TenantId(offender), &cap).unwrap();
assert_eq!(off.bytes_in_use(), QUOTA_BYTES);
for i in 0..TENANTS {
if i == offender {
continue;
}
let ctx = reg.get(TenantId(i), &cap).unwrap();
assert_eq!(ctx.bytes_in_use(), 0, "tenant {i} contaminated");
}
}