1use std::cell::Cell;
4use std::sync::atomic::Ordering;
5use std::sync::Arc;
6use std::thread;
7use std::time::{Duration, Instant};
8
9use khive_storage::error::StorageError;
10use khive_storage::StorageCapability;
11use rusqlite::Connection;
12
13use crate::error::SqliteError;
14
15use super::{
16 deny_retired_writer, pool_exhausted_error, CheckpointGuard, ConnectionPool, ReaderAdmission,
17 ReaderGuard, ReaderLease, WriterGuard,
18};
19
20impl ConnectionPool {
21 pub fn reader(&self) -> Result<ReaderGuard<'_>, SqliteError> {
31 self.reader_until(|| false)?.ok_or_else(|| {
32 SqliteError::InvalidData("uncancelled reader checkout stopped unexpectedly".into())
33 })
34 }
35
36 pub(crate) fn reader_until<C>(
42 &self,
43 should_stop: C,
44 ) -> Result<Option<ReaderGuard<'_>>, SqliteError>
45 where
46 C: Fn() -> bool,
47 {
48 let started = Instant::now();
49 let mut admission_attempt = 0u32;
50 let admission_slot = loop {
51 if should_stop() {
52 return Ok(None);
53 }
54 match Arc::clone(&self.sql_bridge_reader_slots).try_acquire_owned() {
55 Ok(slot) => break slot,
56 Err(tokio::sync::TryAcquireError::Closed) => {
57 return Err(SqliteError::InvalidData(
58 "reader admission semaphore is closed".to_string(),
59 ));
60 }
61 Err(tokio::sync::TryAcquireError::NoPermits) => {}
62 }
63 if started.elapsed() >= self.config.checkout_timeout {
64 self.reader_acquisition_counters.record_checkout_timeout();
65 return Err(pool_exhausted_error(
66 self.config.checkout_timeout,
67 self.max_readers,
68 ));
69 }
70 match admission_attempt {
71 0..=7 => {
72 let spins = 1usize << admission_attempt;
73 for _ in 0..spins {
74 std::hint::spin_loop();
75 }
76 }
77 8..=15 => thread::yield_now(),
78 _ => {
79 let remaining = self
80 .config
81 .checkout_timeout
82 .saturating_sub(started.elapsed());
83 let sleep =
84 Duration::from_micros(50 * (1u64 << (admission_attempt - 16).min(6)));
85 thread::sleep(sleep.min(remaining).min(Duration::from_millis(2)));
86 }
87 }
88 admission_attempt = admission_attempt.saturating_add(1);
89 };
90
91 self.reader_with_admission(
92 ReaderAdmission {
93 slot: admission_slot,
94 started,
95 },
96 should_stop,
97 )
98 }
99
100 pub(crate) async fn acquire_reader_admission(
109 &self,
110 capability: StorageCapability,
111 operation: &'static str,
112 ) -> Result<ReaderAdmission, StorageError> {
113 let context = khive_storage::capture_request_read_context();
114 let started = Instant::now();
115 let stopped = context.stop_reason().is_some();
116 let outcome: Result<Option<ReaderAdmission>, SqliteError> = if stopped {
117 Ok(None)
118 } else {
119 tokio::select! {
120 biased;
121 _ = context.wait_for_stop() => Ok(None),
122 waited = tokio::time::timeout(
123 self.config.checkout_timeout,
124 Arc::clone(&self.sql_bridge_reader_slots).acquire_owned(),
125 ) => match waited {
126 Ok(Ok(slot)) => Ok(Some(ReaderAdmission { slot, started })),
127 Ok(Err(_closed)) => Err(SqliteError::InvalidData(
128 "reader admission semaphore is closed".to_string(),
129 )),
130 Err(_elapsed) => {
131 self.reader_acquisition_counters.record_checkout_timeout();
132 Err(pool_exhausted_error(
133 self.config.checkout_timeout,
134 self.max_readers,
135 ))
136 }
137 },
138 }
139 };
140 match outcome {
141 Ok(Some(admission)) => Ok(admission),
142 Ok(None) => Err(self.reader_checkout_refusal(capability, operation, None)),
143 Err(error) => Err(self.reader_checkout_refusal(capability, operation, Some(error))),
144 }
145 }
146
147 pub(crate) fn reader_with_admission<C>(
154 &self,
155 admission: ReaderAdmission,
156 should_stop: C,
157 ) -> Result<Option<ReaderGuard<'_>>, SqliteError>
158 where
159 C: Fn() -> bool,
160 {
161 let ReaderAdmission {
162 slot: admission_slot,
163 started,
164 } = admission;
165
166 if self.max_readers == 0 {
167 self.ensure_pooled_writer_active()?;
168 loop {
169 if should_stop() {
170 return Ok(None);
171 }
172 let remaining = self
173 .config
174 .checkout_timeout
175 .saturating_sub(started.elapsed());
176 if remaining.is_zero() {
177 self.reader_acquisition_counters.record_checkout_timeout();
178 return Err(pool_exhausted_error(
179 self.config.checkout_timeout,
180 self.max_readers,
181 ));
182 }
183 if let Some(guard) = self
184 .writer
185 .try_lock_for(remaining.min(Duration::from_millis(2)))
186 {
187 self.ensure_pooled_writer_active()?;
188 self.reader_acquisition_counters.record_pooled_checkout();
189 return Ok(Some(ReaderGuard {
190 lease: Some(ReaderLease::Shared(guard)),
191 admission_slot: Some(admission_slot),
192 pool: self,
193 reusable: Cell::new(true),
194 query_in_progress: Cell::new(false),
195 checked_out_at: self.begin_reader_checkout(),
196 dirty: Cell::new(false),
197 operation: None,
198 }));
199 }
200 }
201 }
202
203 let mut attempt = 0u32;
204
205 loop {
206 if should_stop() {
207 return Ok(None);
208 }
209 if let Some(mut conn) = self.readers.pop() {
210 conn.checkouts = conn.checkouts.saturating_add(1);
211 self.reader_acquisition_counters.record_pooled_checkout();
212 return Ok(Some(ReaderGuard {
213 lease: Some(ReaderLease::Pooled(conn)),
214 admission_slot: Some(admission_slot),
215 pool: self,
216 reusable: Cell::new(true),
217 query_in_progress: Cell::new(false),
218 checked_out_at: self.begin_reader_checkout(),
219 dirty: Cell::new(false),
220 operation: None,
221 }));
222 }
223
224 if started.elapsed() >= self.config.checkout_timeout {
225 self.reader_acquisition_counters.record_checkout_timeout();
226 return Err(pool_exhausted_error(
227 self.config.checkout_timeout,
228 self.max_readers,
229 ));
230 }
231
232 match attempt {
233 0..=7 => {
234 let spins = 1usize << attempt;
235 for _ in 0..spins {
236 std::hint::spin_loop();
237 }
238 }
239 8..=15 => thread::yield_now(),
240 _ => {
241 let remaining = self
242 .config
243 .checkout_timeout
244 .saturating_sub(started.elapsed());
245 let sleep = Duration::from_micros(50 * (1u64 << (attempt - 16).min(6)));
246 thread::sleep(sleep.min(remaining).min(Duration::from_millis(2)));
247 }
248 }
249
250 attempt = attempt.saturating_add(1);
251 }
252 }
253
254 #[track_caller]
260 pub fn writer(&self) -> Result<WriterGuard<'_>, SqliteError> {
261 self.writer_with_checkout_probe(true, false)
262 }
263
264 #[track_caller]
267 pub(crate) fn writer_for_admitted_operation(&self) -> Result<WriterGuard<'_>, SqliteError> {
268 self.writer_with_checkout_probe(false, false)
269 }
270
271 pub(super) fn writer_for_checkpoint_operation(&self) -> Result<WriterGuard<'_>, SqliteError> {
274 self.writer_with_checkout_probe(false, true)
275 }
276
277 #[track_caller]
278 fn writer_with_checkout_probe(
279 &self,
280 compatibility_probe: bool,
281 checkpoint_bypass: bool,
282 ) -> Result<WriterGuard<'_>, SqliteError> {
283 if !checkpoint_bypass {
284 self.write_admission.ensure_settled()?;
285 }
286 self.ensure_pooled_writer_active()?;
287 let volume_lease = if checkpoint_bypass {
288 None
289 } else {
290 self.write_admission.acquire()?
291 };
292 let Some(guard) = self.writer.try_lock_for(self.config.checkout_timeout) else {
293 self.writer_acquisition_counters
294 .pooled_timeouts
295 .fetch_add(1, Ordering::Relaxed);
296 let message = format!(
297 "timed out after {:?} waiting for sqlite writer connection",
298 self.config.checkout_timeout
299 );
300 crate::timeout_sink::emit_timeout(
301 &crate::timeout_sink::db_label(self),
302 crate::timeout_sink::Site::PoolAdmission,
303 &message,
304 Some(
305 self.config
306 .checkout_timeout
307 .as_millis()
308 .min(u128::from(u64::MAX)) as u64,
309 ),
310 );
311 return Err(SqliteError::WriterPoolCheckoutTimeout {
312 timeout: self.config.checkout_timeout,
313 });
314 };
315 self.ensure_pooled_writer_active()?;
316 #[cfg(any(unix, windows))]
317 if let Some(path) = self.identity_path.as_deref() {
318 self.verify_opened_file_identity(path)?;
319 self.verify_connection_file_identity(&guard, path)?;
320 }
321 if compatibility_probe && !checkpoint_bypass {
325 self.write_admission.check()?;
326 }
327 self.writer_acquisition_counters
328 .pooled_acquisitions
329 .fetch_add(1, Ordering::Relaxed);
330 Ok(WriterGuard {
331 guard,
332 origin: self.origin(),
333 pool: self,
334 admission: self.write_admission.as_ref(),
335 _volume_lease: volume_lease,
336 })
337 }
338
339 #[track_caller]
344 pub fn try_writer(&self) -> Result<WriterGuard<'_>, SqliteError> {
345 self.writer()
346 }
347
348 #[cfg(test)]
351 #[track_caller]
352 pub(super) fn writer_until<C>(
353 &self,
354 should_stop: C,
355 ) -> Result<Option<WriterGuard<'_>>, SqliteError>
356 where
357 C: Fn() -> bool,
358 {
359 self.writer_until_with_checkout_probe(should_stop, true)
360 }
361
362 #[track_caller]
363 pub(crate) fn writer_until_for_admitted_operation<C>(
364 &self,
365 should_stop: C,
366 ) -> Result<Option<WriterGuard<'_>>, SqliteError>
367 where
368 C: Fn() -> bool,
369 {
370 self.writer_until_with_checkout_probe(should_stop, false)
371 }
372
373 #[track_caller]
374 fn writer_until_with_checkout_probe<C>(
375 &self,
376 should_stop: C,
377 compatibility_probe: bool,
378 ) -> Result<Option<WriterGuard<'_>>, SqliteError>
379 where
380 C: Fn() -> bool,
381 {
382 self.ensure_pooled_writer_active()?;
383 if should_stop() {
384 return Ok(None);
385 }
386 let volume_lease = self.write_admission.acquire()?;
387 let started = Instant::now();
388 loop {
389 if should_stop() {
390 return Ok(None);
391 }
392 let remaining = self
393 .config
394 .checkout_timeout
395 .saturating_sub(started.elapsed());
396 if let Some(guard) = self
397 .writer
398 .try_lock_for(remaining.min(Duration::from_millis(2)))
399 {
400 if should_stop() {
403 return Ok(None);
404 }
405 self.ensure_pooled_writer_active()?;
406 #[cfg(any(unix, windows))]
407 if let Some(path) = self.identity_path.as_deref() {
408 self.verify_opened_file_identity(path)?;
409 self.verify_connection_file_identity(&guard, path)?;
410 }
411 if compatibility_probe {
412 self.write_admission.check()?;
413 }
414 self.writer_acquisition_counters
415 .pooled_acquisitions
416 .fetch_add(1, Ordering::Relaxed);
417 return Ok(Some(WriterGuard {
418 guard,
419 origin: self.origin(),
420 pool: self,
421 admission: self.write_admission.as_ref(),
422 _volume_lease: volume_lease,
423 }));
424 }
425 if started.elapsed() >= self.config.checkout_timeout {
426 self.writer_acquisition_counters
427 .pooled_timeouts
428 .fetch_add(1, Ordering::Relaxed);
429 let message = format!(
430 "timed out after {:?} waiting for sqlite writer connection",
431 self.config.checkout_timeout
432 );
433 crate::timeout_sink::emit_timeout(
434 &crate::timeout_sink::db_label(self),
435 crate::timeout_sink::Site::PoolAdmission,
436 &message,
437 Some(
438 self.config
439 .checkout_timeout
440 .as_millis()
441 .min(u128::from(u64::MAX)) as u64,
442 ),
443 );
444 return Err(SqliteError::WriterPoolCheckoutTimeout {
445 timeout: self.config.checkout_timeout,
446 });
447 }
448 }
449 }
450
451 pub fn try_checkpoint_nowait(&self) -> Result<CheckpointGuard<'_>, SqliteError> {
469 self.ensure_pooled_writer_active()?;
470 let guard = self.writer.try_lock().ok_or_else(|| {
471 SqliteError::InvalidData(
472 "writer connection busy (checkpoint skipped this tick)".to_string(),
473 )
474 })?;
475 self.ensure_pooled_writer_active()?;
476 Ok(CheckpointGuard { guard })
477 }
478
479 pub(crate) fn retire_pooled_writer(&self, conn: &Connection) {
480 self.pooled_writer_retired.store(true, Ordering::Release);
481 if let Err(error) = conn.authorizer(Some(deny_retired_writer)) {
482 tracing::error!(target: "khive_db::pool",
483 %error,
484 "failed to install the retired pooled-writer quarantine authorizer"
485 );
486 }
487 }
488
489 #[cfg(test)]
492 pub(crate) fn probe_retired_pooled_writer_for_test(&self) -> rusqlite::Result<i64> {
493 self.writer
494 .lock()
495 .query_row("SELECT 1", [], |row| row.get(0))
496 }
497
498 #[cfg(test)]
502 pub(crate) fn leave_pooled_writer_transaction_open_for_test(&self) -> rusqlite::Result<()> {
503 self.writer.lock().execute_batch("BEGIN IMMEDIATE")
504 }
505
506 pub(super) fn ensure_pooled_writer_active(&self) -> Result<(), SqliteError> {
507 if self.pooled_writer_retired.load(Ordering::Acquire) {
508 return Err(SqliteError::InvalidData(
509 "pooled writer connection retired after a terminal transaction fault".to_string(),
510 ));
511 }
512 Ok(())
513 }
514}