1use std::collections::BTreeMap;
4use std::path::Path;
5use std::sync::atomic::{AtomicBool, AtomicU64};
6use std::sync::{Arc, OnceLock};
7
8use crossbeam_queue::ArrayQueue;
9use khive_storage::tx_registry::TxOrigin;
10use khive_storage::CapacityUnavailablePhase;
11use parking_lot::Mutex;
12use rusqlite::Connection;
13use tokio::sync::Semaphore;
14
15use crate::disk_guard_config::{resolve_disk_guard_config, DEFAULT_DISK_GUARD_DEADLINE_MS};
16use crate::error::SqliteError;
17#[cfg(any(unix, windows))]
18use crate::file_identity::database_file_identity;
19
20#[cfg(any(unix, windows))]
21use super::{claimed_file_identity, database_file_identity_if_exists, opened_sqlite_file_identity};
22use super::{
23 code_map, configure_writer_connection, deny_retired_writer, effective_reader_count,
24 initialize_database_id_with_admission, mint_db_identity, open_writer_connection,
25 read_database_id, read_only_open_target, refuse_home_data_store_in_tests,
26 validate_wal_ceiling_at_open, validate_write_admission_deadline, CheckpointOwnershipGate,
27 ConnectionPool, PoolConfig, PoolIdentityRegistration, ReaderAcquisitionCounters,
28 WriteAdmission, WriterAcquisitionCounters,
29};
30#[cfg(test)]
31use super::{run_identity_open_hook, IdentityOpenStage};
32
33impl ConnectionPool {
34 pub fn new(config: PoolConfig) -> Result<Self, SqliteError> {
42 refuse_home_data_store_in_tests(&config)?;
43 validate_write_admission_deadline(config.write_admission_deadline_ms)?;
44 if let Some(policy) = config.disk_guard_config {
45 policy.validate()?;
46 }
47 config.wal_ceiling.validate_static(
48 config.path.is_some(),
49 config.wal_mode,
50 config.read_only,
51 )?;
52 code_map::validate_pool(&config)?;
53 if config.path.is_some() && !config.read_only {
54 let lock_dir =
55 crate::disk_guard_config::require_volume_lock_dir(config.volume_lock_dir.clone())?;
56 if !lock_dir.is_absolute() {
57 return Err(SqliteError::CapacityUnavailable {
58 phase: CapacityUnavailablePhase::Lock,
59 message: "configured volume-lock directory is not absolute".to_string(),
60 });
61 }
62 }
63
64 let mut config = config;
68 let inert_memory_queue_request =
69 config.path.is_none() && config.write_queue_enabled == Some(true);
70 config.write_queue_enabled =
71 Some(config.write_queue_enabled.unwrap_or(config.path.is_some()));
72 if inert_memory_queue_request {
73 tracing::warn!(target: "khive_db::pool",
74 "write queue explicitly requested for an in-memory pool; it is inert because \
75 in-memory pools cannot host a writer task"
76 );
77 }
78
79 let (origin, identity_path) = match config.path.as_ref() {
84 Some(path) if config.code_map_vfs.is_some() => code_map::guarded_identity(path),
85 Some(path) => {
86 let (identity, canonical) = mint_db_identity(path)?;
87 (TxOrigin::Database(identity), Some(canonical))
88 }
89 None => (TxOrigin::Memory, None),
90 };
91 let (write_admission, disk_guard_config) = if !config.read_only {
92 if identity_path.as_deref().and_then(Path::parent).is_some() {
93 let disk_guard = match config.disk_guard_config {
94 Some(policy) => policy,
95 None => resolve_disk_guard_config(None, None)?,
96 };
97 disk_guard.validate()?;
98 if disk_guard.legacy_environment_present {
99 tracing::warn!(target: "khive_db::pool",
100 "legacy SQLite reserve setting is deprecated; \
101 use KHIVE_SQLITE_DISK_RESERVE_BYTES"
102 );
103 }
104 if disk_guard.reserve_bytes == 0 {
105 tracing::warn!(target: "khive_db::pool",
106 "SQLite disk reserve is explicitly zero; \
107 new logical writes will not be floor-refused"
108 );
109 }
110 (
111 Arc::new(WriteAdmission::new(
112 identity_path.clone(),
113 disk_guard.reserve_bytes,
114 disk_guard.guard_deadline_ms,
115 config.volume_lock_dir.clone(),
116 )?),
117 Some(disk_guard),
118 )
119 } else {
120 (
121 Arc::new(WriteAdmission::new(
122 None,
123 0,
124 DEFAULT_DISK_GUARD_DEADLINE_MS,
125 None,
126 )?),
127 None,
128 )
129 }
130 } else {
131 (
132 Arc::new(WriteAdmission::new(
133 None,
134 0,
135 DEFAULT_DISK_GUARD_DEADLINE_MS,
136 None,
137 )?),
138 None,
139 )
140 };
141 let read_only_open_target = read_only_open_target(&config, identity_path.as_deref())?;
142 #[cfg(any(unix, windows))]
143 let identity_before_open = identity_path
144 .as_deref()
145 .map(database_file_identity_if_exists)
146 .transpose()?
147 .flatten();
148 #[cfg(any(unix, windows))]
149 claimed_file_identity::verify_before_open(&config, identity_before_open)?;
150 let retirement_connection = Connection::open_in_memory()?;
151 retirement_connection.authorizer(Some(deny_retired_writer))?;
152 let mut initialization_lease = None;
154 let mut writer = open_writer_connection(
155 &config,
156 read_only_open_target.as_deref(),
157 identity_path.as_deref(),
158 )?;
159 validate_wal_ceiling_at_open(&writer, &config)?;
160 writer.busy_timeout(config.busy_timeout)?;
163 let initial_database_id = if identity_path.is_some() {
167 read_database_id(&writer)?
168 } else {
169 None
170 };
171 #[cfg(test)]
172 if let Some(path) = identity_path.as_deref() {
173 run_identity_open_hook(
174 path,
175 IdentityOpenStage::AfterMainOpenBeforeFirstStat,
176 Some(&writer),
177 );
178 }
179 #[cfg(any(unix, windows))]
180 let identity_before_write = identity_path
181 .as_deref()
182 .map(database_file_identity)
183 .transpose()?;
184 #[cfg(any(unix, windows))]
185 if identity_before_open.is_some() && identity_before_open != identity_before_write {
186 return Err(SqliteError::InvalidData(
187 "database file identity changed while opening the pool".to_string(),
188 ));
189 }
190 #[cfg(any(unix, windows))]
191 if let Some(path) = identity_path.as_deref() {
192 let opened = opened_sqlite_file_identity(&writer, path)?;
193 if identity_before_write != Some(opened) {
194 return Err(SqliteError::InvalidData(
195 "database file identity changed while opening the pool".to_string(),
196 ));
197 }
198 }
199 write_admission.verify_current_volume()?;
202 let opened_database_id = if identity_path.is_some()
203 && !config.read_only
204 && initial_database_id.is_none()
205 {
206 match write_admission.check() {
207 Ok(()) => {
208 initialization_lease = write_admission.acquire()?;
209 match initialize_database_id_with_admission(&mut writer, Some(&write_admission))
210 {
211 Ok(id) => Some(id),
212 Err(SqliteError::CapacityFloor { .. }) => None,
213 Err(error) => return Err(error),
214 }
215 }
216 Err(SqliteError::CapacityFloor { .. }) => None,
220 Err(error) => return Err(error),
221 }
222 } else {
223 initial_database_id
224 };
225 drop(initialization_lease.take());
226 #[cfg(test)]
227 if let Some(path) = identity_path.as_deref() {
228 run_identity_open_hook(
229 path,
230 IdentityOpenStage::AfterInitialIdentityWrite,
231 Some(&writer),
232 );
233 }
234 #[cfg(any(unix, windows))]
235 let opened_file_identity = identity_path
236 .as_deref()
237 .map(|path| opened_sqlite_file_identity(&writer, path))
238 .transpose()?;
239 #[cfg(any(unix, windows))]
240 if identity_before_write != opened_file_identity {
241 return Err(SqliteError::InvalidData(
242 "database file identity changed while opening the pool".to_string(),
243 ));
244 }
245 let wal_enabled = configure_writer_connection(&writer, &config)?;
246 let max_readers = effective_reader_count(&config, wal_enabled);
247
248 let readers = ArrayQueue::new(max_readers.max(1));
249
250 #[cfg(any(test, feature = "test-support"))]
251 let statement_observer = crate::statement_observer::StatementObserverHub::new()?;
252 #[cfg(any(test, feature = "test-support"))]
253 crate::statement_observer::install(&writer, &statement_observer)?;
254
255 let mut pool = Self {
256 writer: Arc::new(Mutex::new(writer)),
257 retirement_connection: Mutex::new(Some(retirement_connection)),
258 #[cfg(any(test, feature = "test-support"))]
259 statement_observer,
260 main_pool_generation: OnceLock::new(),
261 checkpoint_ownership: CheckpointOwnershipGate::new(),
262 pooled_writer_retired: AtomicBool::new(false),
263 writer_acquisition_counters: Arc::new(WriterAcquisitionCounters::default()),
264 write_admission,
265 disk_guard_config,
266 reader_acquisition_counters: ReaderAcquisitionCounters::default(),
267 reader_checkouts: Mutex::new(BTreeMap::new()),
268 search_dispatches: Mutex::new(BTreeMap::new()),
269 note_candidate_hydration_rows: AtomicU64::new(0),
270 readers,
271 max_readers,
272 config,
273 read_only_open_target,
274 sql_bridge_reader_slots: Arc::new(Semaphore::new(max_readers.max(1))),
275 sql_bridge_writer_slots: Arc::new(Semaphore::new(1)),
276 writer_task: OnceLock::new(),
277 writer_task_join: Mutex::new(None),
278 writer_task_join_stored: AtomicBool::new(false),
279 origin,
280 identity_path,
281 #[cfg(any(unix, windows))]
282 opened_file_identity,
283 opened_database_id,
284 identity_registration: None,
285 #[cfg(test)]
286 writer_task_spawn_count: std::sync::atomic::AtomicUsize::new(0),
287 };
288
289 for _ in 0..pool.max_readers {
290 let conn = pool.open_pooled_reader()?;
291 pool.readers
292 .push(conn)
293 .expect("reader queue must have capacity during pool initialization");
294 }
295
296 if !pool.config.read_only {
301 crate::timeout_sink::init(
302 pool.canonical_path().and_then(Path::parent),
303 &crate::timeout_sink::db_label(&pool),
304 );
305 }
306
307 pool.identity_registration = pool.canonical_path().map(PoolIdentityRegistration::new);
308 Ok(pool)
309 }
310}