1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! Tenant engine pool — lazy-load YantrikDB instances per database.
//!
//! Each tenant gets an isolated YantrikDB engine backed by its own SQLite file.
//! Engines are cached in memory and shared across connections to the same database.
use parking_lot::Mutex;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use yantrikdb::YantrikDB;
use crate::config::ServerConfig;
use crate::control::{ControlDb, DatabaseRecord};
use crate::embedder::FastEmbedder;
pub struct TenantPool {
engines: Mutex<HashMap<i64, Arc<YantrikDB>>>,
data_dir: PathBuf,
embedding_dim: usize,
embedder: Option<FastEmbedder>,
master_key: Option<[u8; 32]>,
}
impl TenantPool {
pub fn new(
config: &ServerConfig,
embedder: Option<FastEmbedder>,
master_key: Option<[u8; 32]>,
) -> Self {
Self {
engines: Mutex::new(HashMap::new()),
data_dir: config.server.data_dir.clone(),
embedding_dim: config.embedding.dim,
embedder,
master_key,
}
}
/// Whether encryption is enabled for engines created by this pool.
///
/// Not currently called — reserved for /v1/admin/status surfacing of
/// encryption state and for startup diagnostics.
#[allow(dead_code)]
pub fn is_encrypted(&self) -> bool {
self.master_key.is_some()
}
/// Get or create an engine for the given database.
pub fn get_engine(&self, db_record: &DatabaseRecord) -> anyhow::Result<Arc<YantrikDB>> {
let mut engines = self.engines.lock();
if let Some(engine) = engines.get(&db_record.id) {
return Ok(Arc::clone(engine));
}
// Create the database directory if needed
let db_dir = self.data_dir.join(&db_record.path);
std::fs::create_dir_all(&db_dir)?;
let db_path = db_dir.join("yantrik.db");
let mut engine = if let Some(ref key) = self.master_key {
YantrikDB::new_encrypted(
db_path.to_str().unwrap_or("yantrik.db"),
self.embedding_dim,
key,
)?
} else {
YantrikDB::new(db_path.to_str().unwrap_or("yantrik.db"), self.embedding_dim)?
};
// Set the shared embedder if available
if let Some(ref emb) = self.embedder {
engine.set_embedder(emb.boxed());
}
// v0.8.9: drop the server-side Mutex<YantrikDB>. YantrikDB is
// Send+Sync (asserted in engine library); all top-level methods
// take &self with internal locks. The outer Mutex was dead
// serialization that prevented concurrent recall — a single AGI
// could clog a CPU core. Now: Arc<YantrikDB> direct, recalls
// parallelize through engine's read connection pool.
let engine = Arc::new(engine);
engines.insert(db_record.id, Arc::clone(&engine));
tracing::info!(db_name = %db_record.name, db_id = db_record.id, "loaded engine");
Ok(engine)
}
/// Remove an engine from the pool (e.g. on database drop).
///
/// Not currently called — reserved for the planned /v1/admin/drop
/// endpoint which tears down a tenant cleanly.
#[allow(dead_code)]
pub fn evict(&self, db_id: i64) {
let mut engines = self.engines.lock();
engines.remove(&db_id);
}
/// Number of loaded engines.
pub fn loaded_count(&self) -> usize {
self.engines.lock().len()
}
/// Borrow the configured embedder, if any. The HTTP layer uses this
/// to pre-embed `/v1/remember` payloads before delegating to the
/// engine — see issue #19. Pre-embedding lets us fail-fast when
/// the model service hiccups instead of silently writing a row
/// with `embedding=NULL` that then poisons the namespace's
/// similarity-recall path.
pub fn embedder(&self) -> Option<&FastEmbedder> {
self.embedder.as_ref()
}
/// Get the data directory path.
pub fn data_dir(&self) -> &Path {
&self.data_dir
}
}
/// Ensure a "default" database exists in control.db and return its record.
pub fn ensure_default_database(
control: &ControlDb,
data_dir: &Path,
) -> anyhow::Result<DatabaseRecord> {
if let Some(db) = control.get_database("default")? {
return Ok(db);
}
let path = "default";
let db_dir = data_dir.join(path);
std::fs::create_dir_all(&db_dir)?;
let id = control.create_database("default", path)?;
Ok(DatabaseRecord {
id,
name: "default".into(),
path: path.into(),
created_at: String::new(),
})
}