use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
pub type MultiTenancyResult<T> = Result<T, MultiTenancyError>;
#[derive(Debug, Error, Clone, Serialize, Deserialize)]
pub enum MultiTenancyError {
#[error("Tenant not found: {tenant_id}")]
TenantNotFound { tenant_id: String },
#[error("Tenant already exists: {tenant_id}")]
TenantAlreadyExists { tenant_id: String },
#[error("Quota exceeded for tenant {tenant_id}: {resource}")]
QuotaExceeded { tenant_id: String, resource: String },
#[error("Rate limit exceeded for tenant {tenant_id}")]
RateLimitExceeded { tenant_id: String },
#[error("Access denied for tenant {tenant_id}: {reason}")]
AccessDenied { tenant_id: String, reason: String },
#[error("Tenant is suspended: {tenant_id}")]
TenantSuspended { tenant_id: String },
#[error("Invalid tenant configuration: {message}")]
InvalidConfiguration { message: String },
#[error("Isolation violation: {message}")]
IsolationViolation { message: String },
#[error("Billing error: {message}")]
BillingError { message: String },
#[error("Internal error: {message}")]
InternalError { message: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantContext {
pub tenant_id: String,
pub timestamp: DateTime<Utc>,
pub metadata: HashMap<String, String>,
pub auth_token: Option<String>,
pub client_ip: Option<String>,
pub user_agent: Option<String>,
}
impl TenantContext {
pub fn new(tenant_id: impl Into<String>) -> Self {
Self {
tenant_id: tenant_id.into(),
timestamp: Utc::now(),
metadata: HashMap::new(),
auth_token: None,
client_ip: None,
user_agent: None,
}
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn with_auth_token(mut self, token: impl Into<String>) -> Self {
self.auth_token = Some(token.into());
self
}
pub fn with_client_ip(mut self, ip: impl Into<String>) -> Self {
self.client_ip = Some(ip.into());
self
}
pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.user_agent = Some(user_agent.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TenantOperation {
VectorInsert,
VectorSearch,
VectorUpdate,
VectorDelete,
IndexBuild,
BatchOperation,
EmbeddingGeneration,
Reranking,
Custom(u32),
}
impl TenantOperation {
pub fn name(&self) -> &'static str {
match self {
Self::VectorInsert => "vector_insert",
Self::VectorSearch => "vector_search",
Self::VectorUpdate => "vector_update",
Self::VectorDelete => "vector_delete",
Self::IndexBuild => "index_build",
Self::BatchOperation => "batch_operation",
Self::EmbeddingGeneration => "embedding_generation",
Self::Reranking => "reranking",
Self::Custom(_) => "custom",
}
}
pub fn default_cost_weight(&self) -> f64 {
match self {
Self::VectorInsert => 1.0,
Self::VectorSearch => 2.0,
Self::VectorUpdate => 1.5,
Self::VectorDelete => 0.5,
Self::IndexBuild => 10.0,
Self::BatchOperation => 5.0,
Self::EmbeddingGeneration => 3.0,
Self::Reranking => 4.0,
Self::Custom(_) => 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantStatistics {
pub tenant_id: String,
pub total_vectors: usize,
pub total_queries: u64,
pub avg_query_latency_ms: f64,
pub peak_qps: f64,
pub storage_bytes: u64,
pub memory_bytes: u64,
pub api_calls: u64,
pub error_count: u64,
pub last_activity: DateTime<Utc>,
pub operation_counts: HashMap<String, u64>,
pub custom_metrics: HashMap<String, f64>,
}
impl TenantStatistics {
pub fn new(tenant_id: impl Into<String>) -> Self {
Self {
tenant_id: tenant_id.into(),
total_vectors: 0,
total_queries: 0,
avg_query_latency_ms: 0.0,
peak_qps: 0.0,
storage_bytes: 0,
memory_bytes: 0,
api_calls: 0,
error_count: 0,
last_activity: Utc::now(),
operation_counts: HashMap::new(),
custom_metrics: HashMap::new(),
}
}
pub fn record_operation(&mut self, operation: TenantOperation) {
let op_name = operation.name().to_string();
*self.operation_counts.entry(op_name).or_insert(0) += 1;
self.api_calls += 1;
self.last_activity = Utc::now();
}
pub fn record_query(&mut self, latency_ms: f64) {
self.total_queries += 1;
self.record_operation(TenantOperation::VectorSearch);
let alpha = 0.1;
self.avg_query_latency_ms = alpha * latency_ms + (1.0 - alpha) * self.avg_query_latency_ms;
}
pub fn record_error(&mut self) {
self.error_count += 1;
self.last_activity = Utc::now();
}
pub fn update_storage(&mut self, bytes: u64) {
self.storage_bytes = bytes;
}
pub fn update_memory(&mut self, bytes: u64) {
self.memory_bytes = bytes;
}
pub fn set_custom_metric(&mut self, key: impl Into<String>, value: f64) {
self.custom_metrics.insert(key.into(), value);
}
pub fn error_rate(&self) -> f64 {
if self.api_calls == 0 {
0.0
} else {
self.error_count as f64 / self.api_calls as f64
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tenant_context_creation() {
let ctx = TenantContext::new("tenant1")
.with_metadata("region", "us-west")
.with_auth_token("token123")
.with_client_ip("192.168.1.1");
assert_eq!(ctx.tenant_id, "tenant1");
assert_eq!(ctx.metadata.get("region"), Some(&"us-west".to_string()));
assert_eq!(ctx.auth_token, Some("token123".to_string()));
assert_eq!(ctx.client_ip, Some("192.168.1.1".to_string()));
}
#[test]
fn test_tenant_operation_cost_weights() {
assert_eq!(TenantOperation::VectorInsert.default_cost_weight(), 1.0);
assert_eq!(TenantOperation::VectorSearch.default_cost_weight(), 2.0);
assert_eq!(TenantOperation::IndexBuild.default_cost_weight(), 10.0);
}
#[test]
fn test_tenant_statistics() {
let mut stats = TenantStatistics::new("tenant1");
assert_eq!(stats.total_queries, 0);
assert_eq!(stats.api_calls, 0);
stats.record_query(100.0);
assert_eq!(stats.total_queries, 1);
assert_eq!(stats.api_calls, 1);
assert!((stats.avg_query_latency_ms - 10.0).abs() < 1.0);
stats.record_operation(TenantOperation::VectorInsert);
assert_eq!(stats.api_calls, 2);
stats.record_error();
assert_eq!(stats.error_count, 1);
assert!((stats.error_rate() - 0.5).abs() < 0.01); }
#[test]
fn test_multitenance_error_display() {
let error = MultiTenancyError::TenantNotFound {
tenant_id: "tenant1".to_string(),
};
assert!(error.to_string().contains("tenant1"));
let error = MultiTenancyError::QuotaExceeded {
tenant_id: "tenant2".to_string(),
resource: "storage".to_string(),
};
assert!(error.to_string().contains("tenant2"));
assert!(error.to_string().contains("storage"));
}
}