1use rusqlite::Connection;
51use std::collections::HashMap;
52use std::panic::{catch_unwind, AssertUnwindSafe};
53use std::path::{Path, PathBuf};
54use std::sync::{Arc, Mutex, OnceLock};
55use std::time::{Duration, Instant};
56use tokio::sync::{mpsc, oneshot};
57
58use khive_storage::error::{StorageError, WriterTaskRequestState};
59
60use crate::error::SqliteError;
61use crate::pool::{ConnectionPool, WriterAcquisitionCounters};
62
63const WRITER_BEGIN_RETRY_DELAYS: [Duration; 2] =
70 [Duration::from_millis(5), Duration::from_millis(10)];
71
72type WriteOp<R> = Box<dyn FnOnce(&Connection) -> Result<R, StorageError> + Send>;
82
83#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct WriterStageObservation {
89 pub queue_wait_micros: u64,
90 pub transaction_acquire_micros: u64,
91 pub body_micros: u64,
92 pub commit_micros: u64,
93 pub total_micros: u64,
94 pub queue_depth_at_entry: u64,
95 pub observed_at_unix_ms: u64,
96}
97
98static WRITER_STAGE_OBSERVATIONS: OnceLock<
99 Mutex<HashMap<Option<PathBuf>, WriterStageObservation>>,
100> = OnceLock::new();
101
102fn writer_stage_observations() -> &'static Mutex<HashMap<Option<PathBuf>, WriterStageObservation>> {
103 WRITER_STAGE_OBSERVATIONS.get_or_init(|| Mutex::new(HashMap::new()))
104}
105
106fn writer_db_key_from_path(path: Option<&Path>) -> Option<PathBuf> {
107 path.map(Path::to_path_buf)
108}
109
110fn writer_db_key(pool: &ConnectionPool) -> Option<PathBuf> {
111 writer_db_key_from_path(pool.canonical_path())
112}
113
114fn duration_micros(duration: Duration) -> u64 {
115 duration.as_micros().min(u128::from(u64::MAX)) as u64
116}
117
118fn observed_at_unix_ms() -> u64 {
119 std::time::SystemTime::now()
120 .duration_since(std::time::UNIX_EPOCH)
121 .map(|duration| duration.as_millis() as u64)
122 .unwrap_or(0)
123}
124
125pub fn last_writer_stage_observation(pool: &ConnectionPool) -> Option<WriterStageObservation> {
128 writer_stage_observations()
129 .lock()
130 .unwrap_or_else(std::sync::PoisonError::into_inner)
131 .get(&writer_db_key(pool))
132 .cloned()
133}
134
135struct WriteTelemetry {
136 backend_key: Option<PathBuf>,
137 db: String,
138 submitted_at: Instant,
139 queue_depth_at_entry: usize,
140 slow_write_threshold: Option<Duration>,
141}
142
143impl WriteTelemetry {
144 fn new(
145 backend_key: Option<PathBuf>,
146 db: String,
147 queue_depth_at_entry: usize,
148 slow_write_threshold: Option<Duration>,
149 ) -> Self {
150 Self {
151 backend_key,
152 db,
153 submitted_at: Instant::now(),
154 queue_depth_at_entry,
155 slow_write_threshold,
156 }
157 }
158
159 fn queue_wait(&self) -> Duration {
160 self.submitted_at.elapsed()
161 }
162
163 fn finish(
164 self,
165 queue_wait: Duration,
166 transaction_acquire: Duration,
167 body: Duration,
168 commit: Duration,
169 ) {
170 let total = self.submitted_at.elapsed();
171 let observation = WriterStageObservation {
172 queue_wait_micros: duration_micros(queue_wait),
173 transaction_acquire_micros: duration_micros(transaction_acquire),
174 body_micros: duration_micros(body),
175 commit_micros: duration_micros(commit),
176 total_micros: duration_micros(total),
177 queue_depth_at_entry: self.queue_depth_at_entry as u64,
178 observed_at_unix_ms: observed_at_unix_ms(),
179 };
180 writer_stage_observations()
181 .lock()
182 .unwrap_or_else(std::sync::PoisonError::into_inner)
183 .insert(self.backend_key, observation.clone());
184
185 if self
186 .slow_write_threshold
187 .is_some_and(|threshold| total >= threshold)
188 {
189 crate::timeout_sink::emit_slow_write(&self.db, &observation);
190 }
191 }
192}
193
194pub struct WriteRequest<R: Send + 'static> {
209 op: WriteOp<R>,
210 reply: oneshot::Sender<Result<R, StorageError>>,
211 top_level: bool,
212 checkpoint_bypass: bool,
214 vacuum_copy_headroom: bool,
215 telemetry: WriteTelemetry,
216}
217
218mod sealed {
219 pub trait Sealed {
224 fn execute_and_reply_reporting_terminal(
225 self: Box<Self>,
226 conn: &rusqlite::Connection,
227 tx_span: Option<khive_storage::tx_registry::TxHandle>,
228 queue_wait: std::time::Duration,
229 transaction_acquire: std::time::Duration,
230 ) -> Option<khive_storage::error::WriterTaskRequestState>;
231
232 fn execute_and_reply_top_level_reporting_terminal(
233 self: Box<Self>,
234 conn: &rusqlite::Connection,
235 queue_wait: std::time::Duration,
236 ) -> Option<khive_storage::error::WriterTaskRequestState>;
237
238 fn reply_error_after_begin(
239 self: Box<Self>,
240 err: khive_storage::error::StorageError,
241 queue_wait: std::time::Duration,
242 transaction_acquire: std::time::Duration,
243 );
244 }
245}
246
247pub trait AnyWriteRequest: sealed::Sealed + Send {
253 fn execute_and_reply(self: Box<Self>, conn: &Connection);
266
267 fn execute_and_reply_top_level(self: Box<Self>, conn: &Connection);
277
278 fn reply_error(self: Box<Self>, err: StorageError);
290
291 fn is_top_level(&self) -> bool;
295
296 fn is_checkpoint_bypass(&self) -> bool;
297
298 fn needs_vacuum_headroom(&self) -> bool;
299
300 fn queue_wait(&self) -> Duration;
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306enum RollbackDisposition {
307 RolledBack,
308 SideEffectsUnknown,
309}
310
311fn rollback_after_failure(conn: &Connection, failure_context: &'static str) -> RollbackDisposition {
314 match conn.execute_batch("ROLLBACK") {
315 Ok(()) if conn.is_autocommit() => RollbackDisposition::RolledBack,
316 Ok(()) => {
317 tracing::error!(
318 failure_context,
319 "writer transaction: ROLLBACK returned success but the connection is still in a \
320 transaction; request side effects are unknown"
321 );
322 RollbackDisposition::SideEffectsUnknown
323 }
324 Err(rollback_error) => {
325 tracing::error!(
326 error = %rollback_error,
327 failure_context,
328 "writer transaction: rollback after request failure failed; request side effects are \
329 unknown"
330 );
331 RollbackDisposition::SideEffectsUnknown
332 }
333 }
334}
335
336pub(crate) fn execute_wrapped_transaction<R, F>(
339 conn: &Connection,
340 commit_operation: &'static str,
341 operation: F,
342) -> (Result<R, StorageError>, Option<WriterTaskRequestState>)
343where
344 F: FnOnce(&Connection) -> Result<R, StorageError>,
345{
346 let profiled = execute_wrapped_transaction_profiled(conn, commit_operation, None, operation);
347 (profiled.result, profiled.terminal_state)
348}
349
350struct ProfiledWrappedTransaction<R> {
351 result: Result<R, StorageError>,
352 terminal_state: Option<WriterTaskRequestState>,
353 body: Duration,
354 commit: Duration,
355}
356
357fn execute_wrapped_transaction_profiled<R, F>(
358 conn: &Connection,
359 commit_operation: &'static str,
360 db: Option<&str>,
361 operation: F,
362) -> ProfiledWrappedTransaction<R>
363where
364 F: FnOnce(&Connection) -> Result<R, StorageError>,
365{
366 let body_started = Instant::now();
367 let operation_outcome = catch_unwind(AssertUnwindSafe(|| operation(conn)));
368 let body = body_started.elapsed();
369
370 match operation_outcome {
371 Ok(Ok(value)) => {
372 let commit_started = Instant::now();
373 let commit_outcome = conn.execute_batch("COMMIT");
374 let commit = commit_started.elapsed();
375 match commit_outcome {
376 Ok(()) if conn.is_autocommit() => ProfiledWrappedTransaction {
377 result: Ok(value),
378 terminal_state: None,
379 body,
380 commit,
381 },
382 Ok(()) => {
383 tracing::error!(
384 "writer transaction: COMMIT returned success but the connection is still in \
385 a transaction; request side effects are unknown"
386 );
387 let request_state = WriterTaskRequestState::SideEffectsUnknown;
388 ProfiledWrappedTransaction {
389 result: Err(writer_task_terminated(request_state)),
390 terminal_state: Some(request_state),
391 body,
392 commit,
393 }
394 }
395 Err(commit_error) => {
396 if let Some(db) = db {
397 crate::timeout_sink::maybe_emit_sqlite_full(db, &commit_error);
398 }
399 match rollback_after_failure(conn, "commit failure") {
400 RollbackDisposition::RolledBack => ProfiledWrappedTransaction {
401 result: Err(StorageError::WriterTaskRequestFailed {
402 request_state: WriterTaskRequestState::TransactionRolledBack,
403 source: Box::new(StorageError::Pool {
404 operation: commit_operation.into(),
405 message: commit_error.to_string(),
406 }),
407 }),
408 terminal_state: None,
409 body,
410 commit,
411 },
412 RollbackDisposition::SideEffectsUnknown => {
413 let request_state = WriterTaskRequestState::SideEffectsUnknown;
414 ProfiledWrappedTransaction {
415 result: Err(writer_task_terminated(request_state)),
416 terminal_state: Some(request_state),
417 body,
418 commit,
419 }
420 }
421 }
422 }
423 }
424 }
425 Ok(Err(operation_error)) => {
426 match rollback_after_failure(conn, "request operation failure") {
427 RollbackDisposition::RolledBack => ProfiledWrappedTransaction {
428 result: Err(StorageError::WriterTaskRequestFailed {
429 request_state: WriterTaskRequestState::TransactionRolledBack,
430 source: Box::new(operation_error),
431 }),
432 terminal_state: None,
433 body,
434 commit: Duration::ZERO,
435 },
436 RollbackDisposition::SideEffectsUnknown => {
437 if let Some(db) = db {
441 crate::timeout_sink::maybe_emit_sqlite_full(db, &operation_error);
442 }
443 let request_state = WriterTaskRequestState::SideEffectsUnknown;
444 ProfiledWrappedTransaction {
445 result: Err(writer_task_terminated(request_state)),
446 terminal_state: Some(request_state),
447 body,
448 commit: Duration::ZERO,
449 }
450 }
451 }
452 }
453 Err(_panic_payload) => {
454 let request_state = match rollback_after_failure(conn, "request panic") {
455 RollbackDisposition::RolledBack => WriterTaskRequestState::TransactionRolledBack,
456 RollbackDisposition::SideEffectsUnknown => {
457 WriterTaskRequestState::SideEffectsUnknown
458 }
459 };
460 ProfiledWrappedTransaction {
461 result: Err(writer_task_terminated(request_state)),
462 terminal_state: Some(request_state),
463 body,
464 commit: Duration::ZERO,
465 }
466 }
467 }
468}
469
470impl<R: Send + 'static> sealed::Sealed for WriteRequest<R> {
471 fn execute_and_reply_reporting_terminal(
472 self: Box<Self>,
473 conn: &Connection,
474 tx_span: Option<khive_storage::tx_registry::TxHandle>,
475 queue_wait: Duration,
476 transaction_acquire: Duration,
477 ) -> Option<WriterTaskRequestState> {
478 let WriteRequest {
482 op,
483 reply,
484 telemetry,
485 ..
486 } = *self;
487 let profiled = execute_wrapped_transaction_profiled(
488 conn,
489 "writer_task_commit",
490 Some(&telemetry.db),
491 op,
492 );
493 if let Err(error) = &profiled.result {
494 crate::timeout_sink::maybe_emit_sqlite_full(&telemetry.db, error);
495 }
496 drop(tx_span);
501 telemetry.finish(
502 queue_wait,
503 transaction_acquire,
504 profiled.body,
505 profiled.commit,
506 );
507 let _ = reply.send(profiled.result);
510 profiled.terminal_state
511 }
512
513 fn execute_and_reply_top_level_reporting_terminal(
514 self: Box<Self>,
515 conn: &Connection,
516 queue_wait: Duration,
517 ) -> Option<WriterTaskRequestState> {
518 let WriteRequest {
519 op,
520 reply,
521 telemetry,
522 ..
523 } = *self;
524 let body_started = Instant::now();
525 let outcome = catch_unwind(AssertUnwindSafe(|| op(conn)));
526 let body = body_started.elapsed();
527 let telemetry_db = telemetry.db.clone();
528 telemetry.finish(queue_wait, Duration::ZERO, body, Duration::ZERO);
529 match outcome {
530 Ok(outcome) if conn.is_autocommit() => {
531 if let Err(error) = &outcome {
534 crate::timeout_sink::maybe_emit_sqlite_full(&telemetry_db, error);
535 }
536 let _ = reply.send(outcome);
537 None
538 }
539 Ok(_outcome) => {
540 tracing::error!(
541 "writer task: top-level request returned with an open transaction; request \
542 side effects are unknown"
543 );
544 let request_state = WriterTaskRequestState::SideEffectsUnknown;
545 let _ = reply.send(Err(writer_task_terminated(request_state)));
546 Some(request_state)
547 }
548 Err(_panic_payload) => {
549 let request_state = WriterTaskRequestState::SideEffectsUnknown;
553 let _ = reply.send(Err(writer_task_terminated(request_state)));
554 Some(request_state)
555 }
556 }
557 }
558
559 fn reply_error_after_begin(
560 self: Box<Self>,
561 err: StorageError,
562 queue_wait: Duration,
563 transaction_acquire: Duration,
564 ) {
565 let WriteRequest {
566 reply, telemetry, ..
567 } = *self;
568 telemetry.finish(
569 queue_wait,
570 transaction_acquire,
571 Duration::ZERO,
572 Duration::ZERO,
573 );
574 let _ = reply.send(Err(err));
575 }
576}
577
578impl<R: Send + 'static> AnyWriteRequest for WriteRequest<R> {
579 fn execute_and_reply(self: Box<Self>, conn: &Connection) {
580 let queue_wait = self.queue_wait();
581 let _ = sealed::Sealed::execute_and_reply_reporting_terminal(
582 self,
583 conn,
584 None,
585 queue_wait,
586 Duration::ZERO,
587 );
588 }
589
590 fn execute_and_reply_top_level(self: Box<Self>, conn: &Connection) {
591 let queue_wait = self.queue_wait();
592 let _ =
593 sealed::Sealed::execute_and_reply_top_level_reporting_terminal(self, conn, queue_wait);
594 }
595
596 fn reply_error(self: Box<Self>, err: StorageError) {
597 let queue_wait = self.queue_wait();
598 sealed::Sealed::reply_error_after_begin(self, err, queue_wait, Duration::ZERO);
599 }
600
601 fn is_top_level(&self) -> bool {
602 self.top_level
603 }
604
605 fn is_checkpoint_bypass(&self) -> bool {
606 self.checkpoint_bypass
607 }
608
609 fn needs_vacuum_headroom(&self) -> bool {
610 self.vacuum_copy_headroom
611 }
612
613 fn queue_wait(&self) -> Duration {
614 self.telemetry.queue_wait()
615 }
616}
617
618fn writer_task_terminated(request_state: WriterTaskRequestState) -> StorageError {
619 StorageError::WriterTaskTerminated { request_state }
620}
621
622fn writer_task_begin_error(error: rusqlite::Error, busy_timeout: Duration) -> StorageError {
623 if crate::timeout_sink::is_busy_or_locked(&error) {
624 StorageError::WriterTaskBusy {
625 timeout_ms: u64::try_from(busy_timeout.as_millis()).unwrap_or(u64::MAX),
626 }
627 } else {
628 StorageError::Pool {
629 operation: "writer_task_begin".into(),
630 message: error.to_string(),
631 }
632 }
633}
634
635#[derive(Clone, Debug)]
639pub struct WriterTaskHandle {
640 tx: mpsc::Sender<Box<dyn AnyWriteRequest + Send>>,
641 backend_key: Option<PathBuf>,
644 db: String,
649 slow_write_threshold: Option<std::time::Duration>,
655 enqueue_timeout: std::time::Duration,
676}
677
678impl WriterTaskHandle {
679 async fn enqueue<R, F>(
693 &self,
694 op: F,
695 ) -> Result<oneshot::Receiver<Result<R, StorageError>>, StorageError>
696 where
697 R: Send + 'static,
698 F: FnOnce(&Connection) -> Result<R, StorageError> + Send + 'static,
699 {
700 self.enqueue_inner(op, false, false, false).await
701 }
702
703 async fn enqueue_inner<R, F>(
707 &self,
708 op: F,
709 top_level: bool,
710 checkpoint_bypass: bool,
711 vacuum_copy_headroom: bool,
712 ) -> Result<oneshot::Receiver<Result<R, StorageError>>, StorageError>
713 where
714 R: Send + 'static,
715 F: FnOnce(&Connection) -> Result<R, StorageError> + Send + 'static,
716 {
717 let (reply_tx, reply_rx) = oneshot::channel();
718 let telemetry = WriteTelemetry::new(
719 self.backend_key.clone(),
720 self.db.clone(),
721 self.queue_depth(),
722 self.slow_write_threshold,
723 );
724 let request = WriteRequest {
725 op: Box::new(op),
726 reply: reply_tx,
727 top_level,
728 checkpoint_bypass,
729 vacuum_copy_headroom,
730 telemetry,
731 };
732
733 self.tx
734 .send(Box::new(request))
735 .await
736 .map_err(|_| writer_task_terminated(WriterTaskRequestState::NotStarted))?;
737
738 Ok(reply_rx)
739 }
740
741 pub async fn send<R, F>(&self, op: F) -> Result<R, StorageError>
748 where
749 R: Send + 'static,
750 F: FnOnce(&Connection) -> Result<R, StorageError> + Send + 'static,
751 {
752 let reply_rx = self.enqueue(op).await?;
753 reply_rx
754 .await
755 .map_err(|_| writer_task_terminated(WriterTaskRequestState::SideEffectsUnknown))?
756 }
757
758 pub async fn send_with_timeout<R, F>(
773 &self,
774 op: F,
775 timeout: std::time::Duration,
776 ) -> Result<R, StorageError>
777 where
778 R: Send + 'static,
779 F: FnOnce(&Connection) -> Result<R, StorageError> + Send + 'static,
780 {
781 let reply_rx = match tokio::time::timeout(timeout, self.enqueue(op)).await {
782 Ok(Ok(reply_rx)) => reply_rx,
783 Ok(Err(e)) => return Err(e),
784 Err(_elapsed) => {
785 let timeout_ms = timeout.as_millis() as u64;
786 crate::timeout_sink::emit_queue_saturation(&self.db, timeout_ms);
787 return Err(StorageError::WriteQueueFull { timeout_ms });
788 }
789 };
790
791 reply_rx
792 .await
793 .map_err(|_| writer_task_terminated(WriterTaskRequestState::SideEffectsUnknown))?
794 }
795
796 pub async fn send_bounded<R, F>(&self, op: F) -> Result<R, StorageError>
807 where
808 R: Send + 'static,
809 F: FnOnce(&Connection) -> Result<R, StorageError> + Send + 'static,
810 {
811 self.send_with_timeout(op, self.enqueue_timeout).await
812 }
813
814 pub async fn send_top_level<R, F>(&self, op: F) -> Result<R, StorageError>
826 where
827 R: Send + 'static,
828 F: FnOnce(&Connection) -> Result<R, StorageError> + Send + 'static,
829 {
830 let reply_rx = self.enqueue_inner(op, true, false, false).await?;
831 reply_rx
832 .await
833 .map_err(|_| writer_task_terminated(WriterTaskRequestState::SideEffectsUnknown))?
834 }
835
836 pub async fn send_top_level_bounded<R, F>(&self, op: F) -> Result<R, StorageError>
841 where
842 R: Send + 'static,
843 F: FnOnce(&Connection) -> Result<R, StorageError> + Send + 'static,
844 {
845 self.send_top_level_bounded_inner(op, false, false).await
846 }
847
848 pub(crate) async fn send_checkpoint_bounded<R, F>(&self, op: F) -> Result<R, StorageError>
851 where
852 R: Send + 'static,
853 F: FnOnce(&Connection) -> Result<R, StorageError> + Send + 'static,
854 {
855 self.send_top_level_bounded_inner(op, true, false).await
856 }
857
858 pub(crate) async fn send_vacuum_bounded<R, F>(&self, op: F) -> Result<R, StorageError>
860 where
861 R: Send + 'static,
862 F: FnOnce(&Connection) -> Result<R, StorageError> + Send + 'static,
863 {
864 self.send_top_level_bounded_inner(op, false, true).await
865 }
866
867 async fn send_top_level_bounded_inner<R, F>(
868 &self,
869 op: F,
870 checkpoint_bypass: bool,
871 vacuum_copy_headroom: bool,
872 ) -> Result<R, StorageError>
873 where
874 R: Send + 'static,
875 F: FnOnce(&Connection) -> Result<R, StorageError> + Send + 'static,
876 {
877 let reply_rx = match tokio::time::timeout(
878 self.enqueue_timeout,
879 self.enqueue_inner(op, true, checkpoint_bypass, vacuum_copy_headroom),
880 )
881 .await
882 {
883 Ok(Ok(reply_rx)) => reply_rx,
884 Ok(Err(e)) => return Err(e),
885 Err(_elapsed) => {
886 let timeout_ms = self.enqueue_timeout.as_millis() as u64;
887 crate::timeout_sink::emit_queue_saturation(&self.db, timeout_ms);
888 return Err(StorageError::WriteQueueFull { timeout_ms });
889 }
890 };
891
892 reply_rx
893 .await
894 .map_err(|_| writer_task_terminated(WriterTaskRequestState::SideEffectsUnknown))?
895 }
896
897 pub fn queue_depth(&self) -> usize {
906 self.tx.max_capacity() - self.tx.capacity()
907 }
908
909 pub fn capacity(&self) -> usize {
912 self.tx.max_capacity()
913 }
914}
915
916pub fn spawn(pool: &ConnectionPool, capacity: usize) -> Result<WriterTaskHandle, SqliteError> {
946 let conn = pool.open_standalone_writer_untracked()?;
950 let acquisition_counters = pool.writer_acquisition_counters();
951 let write_admission = pool.write_admission();
952 let busy_timeout = pool.config().busy_timeout;
953 let origin = pool.origin();
954 let backend_key = writer_db_key(pool);
955 let db = crate::timeout_sink::db_label(pool);
956 let (tx, rx) = mpsc::channel(capacity.max(1));
957 let join = tokio::spawn(run_writer_task(
958 conn,
959 rx,
960 origin,
961 db.clone(),
962 acquisition_counters,
963 write_admission,
964 busy_timeout,
965 ));
966 pool.set_writer_task_join(join);
970 Ok(WriterTaskHandle {
971 tx,
972 backend_key,
973 db,
974 slow_write_threshold: crate::timeout_sink::slow_write_threshold(),
975 enqueue_timeout: std::time::Duration::from_millis(
976 pool.config().write_admission_deadline_ms,
977 ),
978 })
979}
980
981async fn close_and_fail_queued_requests(rx: &mut mpsc::Receiver<Box<dyn AnyWriteRequest + Send>>) {
989 rx.close();
990 while let Some(request) = rx.recv().await {
991 request.reply_error(writer_task_terminated(WriterTaskRequestState::NotStarted));
992 }
993}
994
995fn begin_immediate_with_retry(
999 conn: &Connection,
1000 acquisition_counters: &WriterAcquisitionCounters,
1001 busy_timeout: Duration,
1002 mut set_busy_timeout: impl FnMut(&Connection, Duration) -> rusqlite::Result<()>,
1003) -> (rusqlite::Result<()>, Duration, u32) {
1004 let transaction_acquire_started = Instant::now();
1005 let mut begin_attempt = 1_u32;
1006 let mut retry_delays = WRITER_BEGIN_RETRY_DELAYS.into_iter();
1007 let mut busy_timeout_lowered = false;
1008 let begin_outcome = loop {
1009 match conn.execute_batch("BEGIN IMMEDIATE") {
1010 Ok(()) => break Ok(()),
1011 Err(error) if crate::timeout_sink::is_busy_or_locked(&error) => {
1012 acquisition_counters.record_writer_task_begin_busy();
1018 let Some(delay) = retry_delays.next() else {
1019 break Err(error);
1020 };
1021 let remaining_budget =
1029 busy_timeout.saturating_sub(transaction_acquire_started.elapsed());
1030 if remaining_budget.is_zero() {
1031 break Err(error);
1032 }
1033 if let Err(set_err) = set_busy_timeout(conn, remaining_budget) {
1034 tracing::warn!(
1035 error = %set_err,
1036 "writer task: failed to lower busy_timeout for BEGIN \
1037 retry; surfacing the original busy refusal"
1038 );
1039 break Err(error);
1041 }
1042 busy_timeout_lowered = true;
1043 acquisition_counters.record_writer_task_begin_busy_absorbed();
1046 tracing::debug!(
1047 attempt = begin_attempt,
1048 backoff_ms = delay.as_millis() as u64,
1049 budget_remaining_ms = remaining_budget.as_millis() as u64,
1050 "writer task: BEGIN IMMEDIATE refused busy; retrying before \
1051 request execution"
1052 );
1053 std::thread::sleep(delay);
1054 begin_attempt = begin_attempt.saturating_add(1);
1055 }
1056 Err(error) => break Err(error),
1057 }
1058 };
1059 let transaction_acquire = transaction_acquire_started.elapsed();
1060 if busy_timeout_lowered {
1066 if let Err(restore_err) = set_busy_timeout(conn, busy_timeout) {
1067 tracing::warn!(
1068 error = %restore_err,
1069 "writer task: failed to restore busy_timeout after a BEGIN retry \
1070 sequence"
1071 );
1072 }
1073 }
1074 (begin_outcome, transaction_acquire, begin_attempt)
1075}
1076
1077struct BlockingWriterConnection {
1078 conn: Option<Connection>,
1079 admission: Arc<crate::pool::WriteAdmission>,
1080 volume_lease: Option<crate::disk_guard::VolumeLease>,
1081}
1082
1083impl BlockingWriterConnection {
1084 fn new(conn: Connection, admission: Arc<crate::pool::WriteAdmission>) -> Self {
1085 Self {
1086 conn: Some(conn),
1087 admission,
1088 volume_lease: None,
1089 }
1090 }
1091
1092 fn finish(
1093 mut self,
1094 state: Option<WriterTaskRequestState>,
1095 ) -> (Option<Connection>, Option<WriterTaskRequestState>) {
1096 if state.is_some() {
1097 self.retire();
1098 (None, state)
1099 } else {
1100 (self.conn.take(), None)
1101 }
1102 }
1103
1104 fn retire(&mut self) {
1105 if let Some(conn) = self.conn.take() {
1106 let _ = self.admission.close_retired_connection(conn);
1109 }
1110 }
1111}
1112
1113impl Drop for BlockingWriterConnection {
1114 fn drop(&mut self) {
1115 self.retire();
1116 }
1117}
1118
1119impl std::ops::Deref for BlockingWriterConnection {
1120 type Target = Connection;
1121
1122 fn deref(&self) -> &Connection {
1123 self.conn
1124 .as_ref()
1125 .expect("blocking writer owns its connection until settlement")
1126 }
1127}
1128
1129async fn run_writer_task(
1143 mut conn: Connection,
1144 mut rx: mpsc::Receiver<Box<dyn AnyWriteRequest + Send>>,
1145 origin: khive_storage::tx_registry::TxOrigin,
1146 db: String,
1147 acquisition_counters: Arc<WriterAcquisitionCounters>,
1148 write_admission: Arc<crate::pool::WriteAdmission>,
1149 busy_timeout: Duration,
1150) {
1151 while let Some(request) = rx.recv().await {
1152 let queue_wait = request.queue_wait();
1156 let origin = origin.clone();
1157 let blocking_counters = Arc::clone(&acquisition_counters);
1158 let blocking_admission = Arc::clone(&write_admission);
1159 let blocking_db = db.clone();
1160 let outcome = tokio::task::spawn_blocking(move || {
1161 let mut conn = BlockingWriterConnection::new(conn, Arc::clone(&blocking_admission));
1162 let acquisition_counters = blocking_counters;
1163 if !conn.is_autocommit() {
1168 tracing::error!(
1169 "writer task: connection is not in autocommit mode before request dispatch; \
1170 retiring the poisoned writer without running the request"
1171 );
1172 let request_state = WriterTaskRequestState::NotStarted;
1173 request.reply_error(writer_task_terminated(request_state));
1174 return conn.finish(Some(request_state));
1175 }
1176
1177 conn.volume_lease = if request.is_checkpoint_bypass() {
1181 None
1182 } else {
1183 match blocking_admission.acquire() {
1184 Ok(lease) => lease,
1185 Err(error) => {
1186 request.reply_error(error.into_storage_error(
1187 khive_storage::StorageCapability::Sql,
1188 "writer_task_admission",
1189 ));
1190 return conn.finish(None);
1191 }
1192 }
1193 };
1194
1195 let terminal_state = if request.is_top_level() {
1196 if !request.is_checkpoint_bypass() {
1197 let admission = if request.needs_vacuum_headroom() {
1198 blocking_admission.check_for_vacuum()
1199 } else {
1200 blocking_admission.check()
1201 };
1202 if let Err(error) = admission {
1203 request.reply_error(error.into_storage_error(
1204 khive_storage::StorageCapability::Sql,
1205 "writer_task_admission",
1206 ));
1207 return conn.finish(None);
1208 }
1209 }
1210 acquisition_counters.record_writer_task_acquisition();
1218 sealed::Sealed::execute_and_reply_top_level_reporting_terminal(
1219 request, &conn, queue_wait,
1220 )
1221 } else {
1222 let tx_span = khive_storage::tx_registry::register_scoped(
1223 Some("writer_task_tx".to_string()),
1224 origin,
1225 );
1226 let (begin_outcome, transaction_acquire, begin_attempt) =
1227 begin_immediate_with_retry(
1228 &conn,
1229 &acquisition_counters,
1230 busy_timeout,
1231 Connection::busy_timeout,
1232 );
1233 match begin_outcome {
1234 Ok(()) => {
1235 if let Err(error) = blocking_admission.check() {
1236 let request_state =
1237 match rollback_after_failure(&conn, "capacity admission") {
1238 RollbackDisposition::RolledBack => None,
1239 RollbackDisposition::SideEffectsUnknown => {
1240 Some(WriterTaskRequestState::SideEffectsUnknown)
1241 }
1242 };
1243 drop(tx_span);
1244 let error = if let Some(state) = request_state {
1245 writer_task_terminated(state)
1246 } else {
1247 error.into_storage_error(
1248 khive_storage::StorageCapability::Sql,
1249 "writer_task_admission",
1250 )
1251 };
1252 sealed::Sealed::reply_error_after_begin(
1253 request,
1254 error,
1255 queue_wait,
1256 transaction_acquire,
1257 );
1258 return conn.finish(request_state);
1259 }
1260 acquisition_counters.record_writer_task_acquisition();
1261 sealed::Sealed::execute_and_reply_reporting_terminal(
1262 request,
1263 &conn,
1264 Some(tx_span),
1265 queue_wait,
1266 transaction_acquire,
1267 )
1268 }
1269 Err(e) => {
1270 crate::timeout_sink::maybe_emit_sqlite_full(&blocking_db, &e);
1276 tracing::warn!(
1277 error = %e,
1278 attempts = begin_attempt,
1279 "writer task: BEGIN IMMEDIATE failed; replying an \
1280 error without running the request's operation"
1281 );
1282 drop(tx_span);
1287 let begin_error = writer_task_begin_error(e, busy_timeout);
1294 if !matches!(&begin_error, StorageError::WriterTaskBusy { .. }) {
1295 acquisition_counters.record_writer_task_begin_error();
1296 }
1297 sealed::Sealed::reply_error_after_begin(
1298 request,
1299 begin_error,
1300 queue_wait,
1301 transaction_acquire,
1302 );
1303 None
1304 }
1305 }
1306 };
1307 conn.finish(terminal_state)
1308 })
1309 .await;
1310
1311 match outcome {
1312 Ok((Some(returned_conn), None)) => conn = returned_conn,
1313 Ok((_returned_conn, Some(request_state))) => {
1314 acquisition_counters.record_writer_task_request_failure();
1315 if request_state == WriterTaskRequestState::SideEffectsUnknown {
1316 acquisition_counters.record_writer_task_side_effects_unknown();
1317 }
1318 tracing::error!(
1319 request_state = %request_state,
1320 "writer task reached a terminal request or connection state; closing and \
1321 failing the queue without restarting"
1322 );
1323 crate::timeout_sink::emit_writer_task_retirement(
1324 &db,
1325 &format!("terminal request state: {request_state}"),
1326 );
1327 close_and_fail_queued_requests(&mut rx).await;
1328 return;
1329 }
1330 Ok((None, None)) => unreachable!("a reusable writer returns its connection"),
1331 Err(join_err) => {
1332 acquisition_counters.record_writer_task_request_failure();
1333 tracing::error!(
1334 error = %join_err,
1335 "writer task blocking closure failed outside the request \
1336 panic boundary; closing and failing the queue without restarting"
1337 );
1338 crate::timeout_sink::emit_writer_task_retirement(
1339 &db,
1340 &format!("blocking closure join failure: {join_err}"),
1341 );
1342 close_and_fail_queued_requests(&mut rx).await;
1343 return;
1344 }
1345 }
1346 }
1347}
1348
1349#[cfg(test)]
1350mod tests {
1351 use super::*;
1352 use crate::pool::PoolConfig;
1353 use rusqlite::hooks::{AuthAction, AuthContext, Authorization, TransactionOperation};
1354 use serial_test::serial;
1355
1356 #[test]
1357 fn begin_error_classification_is_code_based_and_narrow() {
1358 for code in [rusqlite::ffi::SQLITE_BUSY, rusqlite::ffi::SQLITE_LOCKED] {
1359 let error = rusqlite::Error::SqliteFailure(
1360 rusqlite::ffi::Error::new(code),
1361 Some("rendered text is irrelevant".to_string()),
1362 );
1363 assert!(matches!(
1364 writer_task_begin_error(error, Duration::from_millis(175)),
1365 StorageError::WriterTaskBusy { timeout_ms: 175 }
1366 ));
1367 }
1368
1369 let structural = rusqlite::Error::SqliteFailure(
1370 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CONSTRAINT),
1371 Some("database is locked".to_string()),
1372 );
1373 assert!(matches!(
1374 writer_task_begin_error(structural, Duration::from_millis(175)),
1375 StorageError::Pool { ref operation, .. } if operation == "writer_task_begin"
1376 ));
1377 }
1378 use std::future::Future;
1379 use std::pin::Pin;
1380 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1381 use std::sync::mpsc as std_mpsc;
1382 use std::sync::{Arc, Mutex};
1383 use std::task::{Context, Poll, Wake, Waker};
1384 use std::time::Duration;
1385
1386 fn file_pool(path: &std::path::Path) -> ConnectionPool {
1387 let cfg = PoolConfig {
1388 path: Some(path.to_path_buf()),
1389 ..PoolConfig::for_test()
1390 };
1391 ConnectionPool::new(cfg).expect("pool open")
1392 }
1393
1394 fn deny_commit_and_rollback(ctx: AuthContext<'_>) -> Authorization {
1395 match ctx.action {
1396 AuthAction::Transaction {
1399 operation: TransactionOperation::Unknown | TransactionOperation::Rollback,
1400 } => Authorization::Deny,
1401 _ => Authorization::Allow,
1402 }
1403 }
1404
1405 fn deny_commit(ctx: AuthContext<'_>) -> Authorization {
1406 match ctx.action {
1407 AuthAction::Transaction {
1408 operation: TransactionOperation::Unknown,
1409 } => Authorization::Deny,
1410 _ => Authorization::Allow,
1411 }
1412 }
1413
1414 fn deny_rollback(ctx: AuthContext<'_>) -> Authorization {
1415 match ctx.action {
1416 AuthAction::Transaction {
1417 operation: TransactionOperation::Rollback,
1418 } => Authorization::Deny,
1419 _ => Authorization::Allow,
1420 }
1421 }
1422
1423 fn assert_writer_task_terminal_state<T: std::fmt::Debug>(
1424 result: Result<T, StorageError>,
1425 expected: WriterTaskRequestState,
1426 ) {
1427 match result {
1428 Err(StorageError::WriterTaskTerminated { request_state }) => {
1429 assert_eq!(request_state, expected)
1430 }
1431 other => panic!("expected WriterTaskTerminated({expected:?}), got {other:?}"),
1432 }
1433 }
1434
1435 struct ParkedWake {
1436 entered: std_mpsc::SyncSender<()>,
1437 release: Mutex<std_mpsc::Receiver<()>>,
1438 }
1439
1440 impl Wake for ParkedWake {
1441 fn wake(self: Arc<Self>) {
1442 self.entered
1443 .send(())
1444 .expect("reply sender must rendezvous with the test");
1445 self.release
1446 .lock()
1447 .unwrap_or_else(|poisoned| poisoned.into_inner())
1448 .recv()
1449 .expect("test must release the parked reply sender");
1450 }
1451 }
1452
1453 fn arm_parked_wake<F: Future>(
1454 mut future: Pin<&mut F>,
1455 ) -> (std_mpsc::Receiver<()>, std_mpsc::Sender<()>) {
1456 let (entered_tx, entered_rx) = std_mpsc::sync_channel(0);
1457 let (release_tx, release_rx) = std_mpsc::channel();
1458 let waker = Waker::from(Arc::new(ParkedWake {
1459 entered: entered_tx,
1460 release: Mutex::new(release_rx),
1461 }));
1462 let mut context = Context::from_waker(&waker);
1463 assert!(
1464 matches!(future.as_mut().poll(&mut context), Poll::Pending),
1465 "writer send must remain pending until its operation replies"
1466 );
1467 (entered_rx, release_tx)
1468 }
1469
1470 fn poll_ready<F: Future>(mut future: Pin<&mut F>) -> F::Output {
1471 let mut context = Context::from_waker(Waker::noop());
1472 match future.as_mut().poll(&mut context) {
1473 Poll::Ready(output) => output,
1474 Poll::Pending => panic!("reply wake must make the writer send ready"),
1475 }
1476 }
1477
1478 fn database_tx_view(pool: &ConnectionPool) -> khive_storage::tx_registry::TxOriginFilter {
1479 match pool.origin() {
1480 khive_storage::tx_registry::TxOrigin::Database(identity) => {
1481 khive_storage::tx_registry::TxOriginFilter::Secondary(identity)
1482 }
1483 other => panic!("expected a file-backed database origin, got {other:?}"),
1484 }
1485 }
1486
1487 async fn wait_for_writer_span_to_close(view: &khive_storage::tx_registry::TxOriginFilter) {
1488 tokio::time::timeout(Duration::from_secs(5), async {
1489 while khive_storage::tx_registry::any_open_labeled(view, "writer_task_tx") {
1490 tokio::task::yield_now().await;
1491 }
1492 })
1493 .await
1494 .expect("writer task transaction span must eventually close");
1495 }
1496
1497 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1498 #[serial(tx_registry)]
1499 async fn writer_task_connection_maintains_rfc3339_expression_indexes() {
1500 let dir = tempfile::tempdir().unwrap();
1501 let path = dir.path().join("writer_task_rfc3339_expression_index.db");
1502 let pool = file_pool(&path);
1503 {
1504 let writer = pool.writer().expect("pooled writer");
1505 writer
1506 .conn()
1507 .execute_batch(
1508 "CREATE TABLE deadlines(id INTEGER PRIMARY KEY, due TEXT);
1509 CREATE INDEX idx_deadlines_strict \
1510 ON deadlines(ifnull(khive_rfc3339_strict_key(due), x''));",
1511 )
1512 .expect("pooled writer registers the key function");
1513 }
1514 let handle = spawn(&pool, 8).expect("writer task spawn");
1515
1516 let inserted = handle
1517 .send(|conn| {
1518 conn.execute(
1519 "INSERT INTO deadlines(id, due) VALUES (1, '2026-01-01T00:00:00Z')",
1520 [],
1521 )
1522 .map_err(|error| StorageError::Pool {
1523 operation: "test_insert".into(),
1524 message: error.to_string(),
1525 })
1526 })
1527 .await
1528 .expect("the writer task's connection maintains the expression index");
1529 assert_eq!(inserted, 1);
1530 }
1531
1532 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1533 #[serial(tx_registry)]
1534 async fn successful_send_reply_waits_for_writer_tx_deregistration() {
1535 let dir = tempfile::tempdir().unwrap();
1536 let path = dir.path().join("writer_task_success_reply_lifecycle.db");
1537 let pool = file_pool(&path);
1538 let view = database_tx_view(&pool);
1539 let handle = spawn(&pool, 8).expect("writer task spawn");
1540 let (op_started_tx, op_started_rx) = std_mpsc::sync_channel(0);
1541 let (op_release_tx, op_release_rx) = std_mpsc::channel();
1542
1543 let send = handle.send(move |_conn| {
1544 op_started_tx
1545 .send(())
1546 .expect("operation must rendezvous with the test");
1547 op_release_rx
1548 .recv()
1549 .expect("test must release the operation");
1550 Ok::<_, StorageError>(())
1551 });
1552 tokio::pin!(send);
1553 let (reply_entered_rx, reply_release_tx) = arm_parked_wake(send.as_mut());
1554
1555 op_started_rx
1556 .recv_timeout(Duration::from_secs(5))
1557 .expect("writer operation must start");
1558 op_release_tx.send(()).expect("release writer operation");
1559 reply_entered_rx
1560 .recv_timeout(Duration::from_secs(5))
1561 .expect("reply sender must wake the waiting caller");
1562
1563 let reply = poll_ready(send.as_mut());
1564 let span_was_open_at_reply =
1565 khive_storage::tx_registry::any_open_labeled(&view, "writer_task_tx");
1566
1567 reply_release_tx
1568 .send(())
1569 .expect("release parked reply sender");
1570 wait_for_writer_span_to_close(&view).await;
1571
1572 reply.expect("committed operation reply");
1573 assert!(
1574 !span_was_open_at_reply,
1575 "a successful caller reply must not become observable while its committed writer_task_tx span remains registered"
1576 );
1577 }
1578
1579 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1580 #[serial(tx_registry)]
1581 async fn begin_failure_reply_waits_for_writer_tx_deregistration() {
1582 let dir = tempfile::tempdir().unwrap();
1583 let path = dir.path().join("writer_task_begin_reply_lifecycle.db");
1584 let cfg = PoolConfig {
1587 path: Some(path.clone()),
1588 volume_lock_dir: Some(dir.path().join("volume-locks")),
1589 busy_timeout: Duration::from_millis(150),
1590 ..PoolConfig::for_test()
1591 };
1592 let pool = ConnectionPool::new(cfg).unwrap();
1593 let view = database_tx_view(&pool);
1594 let handle = spawn(&pool, 8).expect("writer task spawn");
1595 let lock_holder = rusqlite::Connection::open(&path).expect("external writer");
1598 lock_holder
1599 .execute_batch("BEGIN IMMEDIATE")
1600 .expect("hold database write lock");
1601 let op_ran = Arc::new(AtomicBool::new(false));
1602 let op_ran_in_request = Arc::clone(&op_ran);
1603
1604 let send = handle.send(move |_conn| {
1605 op_ran_in_request.store(true, Ordering::SeqCst);
1606 Ok::<_, StorageError>(())
1607 });
1608 tokio::pin!(send);
1609 let (reply_entered_rx, reply_release_tx) = arm_parked_wake(send.as_mut());
1610 reply_entered_rx
1611 .recv_timeout(Duration::from_secs(5))
1612 .expect("BEGIN failure must wake the waiting caller");
1613
1614 let reply = poll_ready(send.as_mut());
1615 let span_was_open_at_reply =
1616 khive_storage::tx_registry::any_open_labeled(&view, "writer_task_tx");
1617
1618 reply_release_tx
1619 .send(())
1620 .expect("release parked reply sender");
1621 wait_for_writer_span_to_close(&view).await;
1622 lock_holder
1623 .execute_batch("ROLLBACK")
1624 .expect("release database write lock");
1625
1626 assert!(
1627 matches!(
1628 &reply,
1629 Err(StorageError::WriterTaskBusy { timeout_ms }) if *timeout_ms == 150
1630 ),
1631 "expected typed retryable writer-task contention, got {reply:?}"
1632 );
1633 assert!(!op_ran.load(Ordering::SeqCst));
1634 assert!(
1635 !span_was_open_at_reply,
1636 "a BEGIN-failure caller reply must not become observable while its writer_task_tx span remains registered"
1637 );
1638 }
1639
1640 #[tokio::test]
1647 #[serial(tx_registry)]
1648 async fn begin_immediate_failure_replies_error_without_running_op() {
1649 let dir = tempfile::tempdir().unwrap();
1654 let path = dir.path().join("writer_task_begin_failure.db");
1655 let cfg = PoolConfig {
1658 path: Some(path.clone()),
1659 volume_lock_dir: Some(dir.path().join("volume-locks")),
1660 busy_timeout: Duration::from_millis(150),
1661 ..PoolConfig::for_test()
1662 };
1663 let pool = ConnectionPool::new(cfg).unwrap();
1664 {
1665 let writer = pool.try_writer().unwrap();
1666 writer
1667 .conn()
1668 .execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
1669 .unwrap();
1670 }
1671
1672 let handle = spawn(&pool, 8).expect("writer task should spawn on a file-backed pool");
1673
1674 let lock_holder = rusqlite::Connection::open(&path).unwrap();
1675 lock_holder.execute_batch("BEGIN IMMEDIATE").unwrap();
1676
1677 let op_ran = Arc::new(AtomicBool::new(false));
1678 let op_ran_clone = Arc::clone(&op_ran);
1679 let result = handle
1680 .send(move |conn| {
1681 op_ran_clone.store(true, Ordering::SeqCst);
1682 conn.execute("INSERT INTO t (id, v) VALUES (99, 'should-not-land')", [])
1683 .map_err(|e| StorageError::Pool {
1684 operation: "test_insert".into(),
1685 message: e.to_string(),
1686 })
1687 })
1688 .await;
1689
1690 assert!(
1691 matches!(
1692 &result,
1693 Err(StorageError::WriterTaskBusy { timeout_ms }) if *timeout_ms == 150
1694 ),
1695 "expected a typed retryable error on contended BEGIN IMMEDIATE, got {result:?}"
1696 );
1697 assert!(
1698 !op_ran.load(Ordering::SeqCst),
1699 "the request's operation closure must never run when BEGIN \
1700 IMMEDIATE fails — running it would land a partial write in \
1701 autocommit mode for a request the caller is told failed"
1702 );
1703
1704 lock_holder.execute_batch("ROLLBACK").unwrap();
1707 drop(lock_holder);
1708
1709 handle
1710 .send(|conn| {
1711 conn.execute("INSERT INTO t (id, v) VALUES (100, 'next-request')", [])
1712 .map_err(|e| StorageError::Pool {
1713 operation: "test_insert_after_busy".into(),
1714 message: e.to_string(),
1715 })
1716 })
1717 .await
1718 .expect("transient contention must not retire the writer task");
1719
1720 let reader = pool.reader().expect("reader");
1721 let count: i64 = reader
1722 .conn()
1723 .query_row("SELECT COUNT(*) FROM t WHERE id IN (99, 100)", [], |row| {
1724 row.get(0)
1725 })
1726 .unwrap();
1727 assert_eq!(
1728 count, 1,
1729 "the failed request must not land, while the next request commits on the same task"
1730 );
1731 }
1732
1733 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1736 #[serial(tx_registry)]
1737 async fn transient_begin_contention_clears_within_budget_and_op_runs_once() {
1738 let dir = tempfile::tempdir().unwrap();
1747 let path = dir.path().join("writer_task_begin_transient_contention.db");
1748 let pool = ConnectionPool::new(PoolConfig {
1751 path: Some(path.clone()),
1752 volume_lock_dir: Some(dir.path().join("volume-locks")),
1753 busy_timeout: Duration::from_millis(500),
1754 ..PoolConfig::for_test()
1755 })
1756 .unwrap();
1757 {
1758 let writer = pool.try_writer().unwrap();
1759 writer
1760 .conn()
1761 .execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY)")
1762 .unwrap();
1763 }
1764 let handle = spawn(&pool, 8).expect("writer task spawn");
1765 let lock_holder = Connection::open(&path).unwrap();
1769 lock_holder.execute_batch("BEGIN IMMEDIATE").unwrap();
1770
1771 let op_runs = Arc::new(AtomicUsize::new(0));
1772 let op_runs_in_request = Arc::clone(&op_runs);
1773 let send_future = handle.send(move |conn| {
1774 op_runs_in_request.fetch_add(1, Ordering::SeqCst);
1775 conn.execute("INSERT INTO t (id) VALUES (1)", [])
1776 .map_err(|error| StorageError::Pool {
1777 operation: "test_insert_after_transient_contention".into(),
1778 message: error.to_string(),
1779 })
1780 });
1781 let release_future = async {
1782 tokio::time::sleep(Duration::from_millis(50)).await;
1783 lock_holder.execute_batch("ROLLBACK").unwrap();
1784 };
1785 let (result, ()) = tokio::join!(send_future, release_future);
1786
1787 assert_eq!(
1788 result.expect("BEGIN IMMEDIATE succeeds once the transient lock clears"),
1789 1
1790 );
1791 assert_eq!(
1792 op_runs.load(Ordering::SeqCst),
1793 1,
1794 "the FnOnce request closure must execute exactly once"
1795 );
1796
1797 let settled = pool.writer_acquisition_snapshot();
1798 assert_eq!(
1799 settled.writer_task_begin_busy, 0,
1800 "contention absorbed inside SQLite's own busy_timeout wait must never \
1801 surface as a Rust-level refusal"
1802 );
1803 assert_eq!(settled.writer_task_begin_busy_absorbed, 0);
1804 let reader = pool.reader().unwrap();
1805 let rows: i64 = reader
1806 .conn()
1807 .query_row("SELECT COUNT(*) FROM t", [], |row| row.get(0))
1808 .unwrap();
1809 assert_eq!(rows, 1, "exactly one closure execution commits one row");
1810 }
1811
1812 include!("writer_task_admission_retry_tests.rs");
1813
1814 #[tokio::test]
1818 #[serial(tx_registry)]
1819 async fn writer_task_executes_op_and_commits() {
1820 let dir = tempfile::tempdir().unwrap();
1821 let path = dir.path().join("writer_task_commit.db");
1822 let pool = file_pool(&path);
1823 {
1824 let writer = pool.try_writer().unwrap();
1825 writer
1826 .conn()
1827 .execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
1828 .unwrap();
1829 }
1830
1831 let handle = spawn(&pool, 8).expect("writer task should spawn on a file-backed pool");
1832
1833 let affected = handle
1834 .send(|conn| {
1835 conn.execute("INSERT INTO t (id, v) VALUES (1, 'hello')", [])
1836 .map_err(|e| StorageError::Pool {
1837 operation: "test_insert".into(),
1838 message: e.to_string(),
1839 })
1840 })
1841 .await
1842 .expect("op should succeed");
1843 assert_eq!(affected, 1);
1844
1845 let reader = pool.reader().expect("reader");
1849 let v: String = reader
1850 .conn()
1851 .query_row("SELECT v FROM t WHERE id = 1", [], |row| row.get(0))
1852 .expect("row must be committed and visible to a reader");
1853 assert_eq!(v, "hello");
1854
1855 let counters = pool.writer_acquisition_snapshot();
1856 assert_eq!(counters.acquisitions, 2);
1857 assert_eq!(counters.pooled_acquisitions, 1);
1858 assert_eq!(counters.standalone_acquisitions, 0);
1859 assert_eq!(counters.writer_task_acquisitions, 1);
1860 assert_eq!(counters.timeouts, 0);
1861 }
1862
1863 #[tokio::test]
1864 async fn writer_task_connection_follows_checkpoint_ownership_claim() {
1865 let dir = tempfile::tempdir().unwrap();
1866 let path = dir.path().join("writer_task_autocheckpoint.db");
1867 let pool = file_pool(&path);
1868 let handle = pool
1872 .writer_task_handle()
1873 .expect("writer task should spawn")
1874 .expect("file-backed pool resolves the write queue on");
1875
1876 let read_pages = |handle: &WriterTaskHandle| {
1877 let handle = handle.clone();
1878 async move {
1879 handle
1880 .send_top_level(|conn| {
1881 conn.pragma_query_value(None, "wal_autocheckpoint", |row| {
1882 row.get::<_, u32>(0)
1883 })
1884 .map_err(|e| StorageError::Pool {
1885 operation: "test_wal_autocheckpoint".into(),
1886 message: e.to_string(),
1887 })
1888 })
1889 .await
1890 .expect("query writer-task connection pragma")
1891 }
1892 };
1893
1894 assert_eq!(
1897 read_pages(&handle).await,
1898 crate::pool::FALLBACK_WAL_AUTOCHECKPOINT_PAGES
1899 );
1900
1901 pool.claim_checkpoint_ownership().expect("claim ownership");
1904 pool.propagate_checkpoint_claim_to_writer_task()
1905 .await
1906 .expect("propagate claim to the running writer task");
1907 assert_eq!(read_pages(&handle).await, 0);
1908 }
1909
1910 #[test]
1911 fn spawn_fails_on_in_memory_pool() {
1912 let cfg = PoolConfig {
1918 path: None,
1919 ..PoolConfig::default()
1920 };
1921 let pool = ConnectionPool::new(cfg).unwrap();
1922 let result = spawn(&pool, 8);
1923 assert!(
1924 result.is_err(),
1925 "in-memory pools must reject spawn, not panic"
1926 );
1927 }
1928
1929 include!("writer_task_queue_capacity_tests.rs");
1930
1931 #[tokio::test]
1932 #[serial(tx_registry)]
1933 async fn operation_failure_with_successful_rollback_reports_finality_once_and_continues() {
1934 let dir = tempfile::tempdir().unwrap();
1935 let path = dir.path().join("writer_task_operation_rollback.db");
1936 let pool = file_pool(&path);
1937 {
1938 let writer = pool.try_writer().unwrap();
1939 writer
1940 .conn()
1941 .execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
1942 .unwrap();
1943 }
1944 let handle = spawn(&pool, 8).expect("writer task spawn");
1945
1946 let executions = Arc::new(AtomicUsize::new(0));
1947 let executions_in_op = Arc::clone(&executions);
1948 let original_error = handle
1949 .send(move |conn| -> Result<(), StorageError> {
1950 executions_in_op.fetch_add(1, Ordering::SeqCst);
1951 conn.execute("INSERT INTO t (id, v) VALUES (1, 'rolled-back')", [])
1952 .map_err(|e| StorageError::Pool {
1953 operation: "test_operation_error_insert".into(),
1954 message: e.to_string(),
1955 })?;
1956 Err(StorageError::Internal(
1957 "intentional operation failure".into(),
1958 ))
1959 })
1960 .await;
1961 match &original_error {
1962 Err(StorageError::WriterTaskRequestFailed {
1963 request_state: WriterTaskRequestState::TransactionRolledBack,
1964 source,
1965 }) => assert!(
1966 matches!(source.as_ref(), StorageError::Internal(message)
1967 if message == "intentional operation failure"),
1968 "the proven-rollback wrapper must retain the typed operation error: {source:?}"
1969 ),
1970 other => panic!(
1971 "a confirmed rollback must carry TransactionRolledBack and preserve the operation error, got {other:?}"
1972 ),
1973 }
1974 assert_eq!(
1975 executions.load(Ordering::SeqCst),
1976 1,
1977 "finality propagation must not replay the request closure"
1978 );
1979
1980 let affected = handle
1981 .send(|conn| {
1982 conn.execute("INSERT INTO t (id, v) VALUES (2, 'committed')", [])
1983 .map_err(|e| StorageError::Pool {
1984 operation: "test_operation_error_followup_insert".into(),
1985 message: e.to_string(),
1986 })
1987 })
1988 .await
1989 .expect("the writer must continue after a confirmed rollback");
1990 assert_eq!(affected, 1);
1991 assert_eq!(
1992 executions.load(Ordering::SeqCst),
1993 1,
1994 "serving a follow-up request must not replay the rolled-back closure"
1995 );
1996
1997 let reader = pool.reader().expect("reader");
1998 let rolled_back: i64 = reader
1999 .conn()
2000 .query_row("SELECT COUNT(*) FROM t WHERE id = 1", [], |row| row.get(0))
2001 .unwrap();
2002 let committed: i64 = reader
2003 .conn()
2004 .query_row("SELECT COUNT(*) FROM t WHERE id = 2", [], |row| row.get(0))
2005 .unwrap();
2006 assert_eq!(rolled_back, 0);
2007 assert_eq!(committed, 1);
2008 }
2009
2010 #[tokio::test]
2011 #[serial(tx_registry)]
2012 async fn commit_failure_with_successful_rollback_reports_finality_once_and_continues() {
2013 let dir = tempfile::tempdir().unwrap();
2014 let path = dir.path().join("writer_task_commit_rollback.db");
2015 let pool = file_pool(&path);
2016 {
2017 let writer = pool.try_writer().unwrap();
2018 writer
2019 .conn()
2020 .execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
2021 .unwrap();
2022 }
2023 let handle = spawn(&pool, 8).expect("writer task spawn");
2024
2025 let executions = Arc::new(AtomicUsize::new(0));
2026 let executions_in_op = Arc::clone(&executions);
2027 let commit_error = handle
2028 .send(move |conn| -> Result<usize, StorageError> {
2029 executions_in_op.fetch_add(1, Ordering::SeqCst);
2030 let affected = conn
2031 .execute("INSERT INTO t (id, v) VALUES (1, 'rolled-back')", [])
2032 .map_err(|e| StorageError::Pool {
2033 operation: "test_commit_error_insert".into(),
2034 message: e.to_string(),
2035 })?;
2036 conn.authorizer(Some(deny_commit))
2037 .map_err(|e| StorageError::Pool {
2038 operation: "test_install_authorizer".into(),
2039 message: e.to_string(),
2040 })?;
2041 Ok(affected)
2042 })
2043 .await;
2044 match &commit_error {
2045 Err(StorageError::WriterTaskRequestFailed {
2046 request_state: WriterTaskRequestState::TransactionRolledBack,
2047 source,
2048 }) => assert!(
2049 matches!(source.as_ref(), StorageError::Pool { operation, .. }
2050 if operation == "writer_task_commit"),
2051 "the proven-rollback wrapper must retain the typed COMMIT error: {source:?}"
2052 ),
2053 other => panic!(
2054 "a confirmed rollback must carry TransactionRolledBack and preserve the COMMIT error, got {other:?}"
2055 ),
2056 }
2057 assert!(
2058 commit_error
2059 .as_ref()
2060 .expect_err("COMMIT must be denied")
2061 .is_retryable(),
2062 "the existing retryable commit-error contract must remain unchanged after a \
2063 confirmed rollback"
2064 );
2065 assert_eq!(
2066 executions.load(Ordering::SeqCst),
2067 1,
2068 "finality propagation must not replay the request closure"
2069 );
2070
2071 let affected = handle
2072 .send(|conn| {
2073 conn.authorizer(None::<fn(AuthContext<'_>) -> Authorization>)
2074 .map_err(|e| StorageError::Pool {
2075 operation: "test_remove_authorizer".into(),
2076 message: e.to_string(),
2077 })?;
2078 conn.execute("INSERT INTO t (id, v) VALUES (2, 'committed')", [])
2079 .map_err(|e| StorageError::Pool {
2080 operation: "test_commit_error_followup_insert".into(),
2081 message: e.to_string(),
2082 })
2083 })
2084 .await
2085 .expect("the writer must continue after the failed COMMIT is rolled back");
2086 assert_eq!(affected, 1);
2087 assert_eq!(
2088 executions.load(Ordering::SeqCst),
2089 1,
2090 "serving a follow-up request must not replay the rolled-back closure"
2091 );
2092
2093 let reader = pool.reader().expect("reader");
2094 let rolled_back: i64 = reader
2095 .conn()
2096 .query_row("SELECT COUNT(*) FROM t WHERE id = 1", [], |row| row.get(0))
2097 .unwrap();
2098 let committed: i64 = reader
2099 .conn()
2100 .query_row("SELECT COUNT(*) FROM t WHERE id = 2", [], |row| row.get(0))
2101 .unwrap();
2102 assert_eq!(rolled_back, 0);
2103 assert_eq!(committed, 1);
2104 }
2105
2106 #[test]
2107 fn top_level_request_returning_with_open_transaction_reports_side_effects_unknown() {
2108 let conn = Connection::open_in_memory().expect("in-memory connection");
2109 conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY)")
2110 .unwrap();
2111 let (reply_tx, mut reply_rx) = oneshot::channel();
2112 let request = WriteRequest {
2113 op: Box::new(|conn| -> Result<usize, StorageError> {
2114 conn.execute_batch("BEGIN IMMEDIATE")
2115 .map_err(|e| StorageError::Pool {
2116 operation: "test_top_level_begin".into(),
2117 message: e.to_string(),
2118 })?;
2119 conn.execute("INSERT INTO t (id) VALUES (1)", [])
2120 .map_err(|e| StorageError::Pool {
2121 operation: "test_top_level_insert".into(),
2122 message: e.to_string(),
2123 })
2124 }),
2125 reply: reply_tx,
2126 top_level: true,
2127 checkpoint_bypass: false,
2128 vacuum_copy_headroom: false,
2129 telemetry: WriteTelemetry::new(None, "test".to_string(), 0, None),
2130 };
2131
2132 let terminal_state = sealed::Sealed::execute_and_reply_top_level_reporting_terminal(
2133 Box::new(request),
2134 &conn,
2135 Duration::ZERO,
2136 );
2137 assert_eq!(
2138 terminal_state,
2139 Some(WriterTaskRequestState::SideEffectsUnknown)
2140 );
2141 let reply = reply_rx
2142 .try_recv()
2143 .expect("active request must receive a typed terminal reply");
2144 assert_writer_task_terminal_state(reply, WriterTaskRequestState::SideEffectsUnknown);
2145 assert!(
2146 !conn.is_autocommit(),
2147 "the fixture must prove the post-request autocommit check observed an open transaction"
2148 );
2149 }
2150
2151 #[test]
2152 fn commit_failure_with_failed_rollback_reports_side_effects_unknown() {
2153 let conn = Connection::open_in_memory().expect("in-memory connection");
2154 conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY); BEGIN IMMEDIATE")
2155 .unwrap();
2156 let executions = Arc::new(AtomicUsize::new(0));
2157 let executions_in_op = Arc::clone(&executions);
2158 let (reply_tx, mut reply_rx) = oneshot::channel();
2159 let request = WriteRequest {
2160 op: Box::new(move |conn| -> Result<usize, StorageError> {
2161 executions_in_op.fetch_add(1, Ordering::SeqCst);
2162 let affected = conn
2163 .execute("INSERT INTO t (id) VALUES (1)", [])
2164 .map_err(|e| StorageError::Pool {
2165 operation: "test_insert_before_commit_failure".into(),
2166 message: e.to_string(),
2167 })?;
2168 conn.authorizer(Some(deny_commit_and_rollback))
2169 .map_err(|e| StorageError::Pool {
2170 operation: "test_install_authorizer".into(),
2171 message: e.to_string(),
2172 })?;
2173 Ok(affected)
2174 }),
2175 reply: reply_tx,
2176 top_level: false,
2177 checkpoint_bypass: false,
2178 vacuum_copy_headroom: false,
2179 telemetry: WriteTelemetry::new(None, "test".to_string(), 0, None),
2180 };
2181
2182 let terminal_state = sealed::Sealed::execute_and_reply_reporting_terminal(
2183 Box::new(request),
2184 &conn,
2185 None,
2186 Duration::ZERO,
2187 Duration::ZERO,
2188 );
2189 assert_eq!(
2190 terminal_state,
2191 Some(WriterTaskRequestState::SideEffectsUnknown)
2192 );
2193 let reply = reply_rx
2194 .try_recv()
2195 .expect("active request must receive a typed terminal reply");
2196 assert_writer_task_terminal_state(reply, WriterTaskRequestState::SideEffectsUnknown);
2197 assert_eq!(executions.load(Ordering::SeqCst), 1);
2198 assert!(
2199 !conn.is_autocommit(),
2200 "the denied COMMIT and ROLLBACK must leave the test connection poisoned"
2201 );
2202 }
2203
2204 #[tokio::test]
2205 #[serial(tx_registry)]
2206 async fn poisoned_connection_retires_before_queued_top_level_request() {
2207 let dir = tempfile::tempdir().unwrap();
2208 let path = dir.path().join("writer_task_rollback_poison.db");
2209 let pool = file_pool(&path);
2210 {
2211 let writer = pool.try_writer().unwrap();
2212 writer
2213 .conn()
2214 .execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
2215 .unwrap();
2216 }
2217 let handle = spawn(&pool, 8).expect("writer task spawn");
2218 let (started_tx, started_rx) = oneshot::channel::<()>();
2219 let (release_tx, release_rx) = std_mpsc::channel::<()>();
2220
2221 let active = tokio::spawn({
2222 let handle = handle.clone();
2223 async move {
2224 handle
2225 .send(move |conn| -> Result<usize, StorageError> {
2226 let affected = conn
2227 .execute("INSERT INTO t (id, v) VALUES (1, 'active')", [])
2228 .map_err(|e| StorageError::Pool {
2229 operation: "test_active_insert".into(),
2230 message: e.to_string(),
2231 })?;
2232 conn.authorizer(Some(deny_commit_and_rollback))
2233 .map_err(|e| StorageError::Pool {
2234 operation: "test_install_authorizer".into(),
2235 message: e.to_string(),
2236 })?;
2237 let _ = started_tx.send(());
2238 release_rx.recv().expect("test must release active op");
2239 Ok(affected)
2240 })
2241 .await
2242 }
2243 });
2244
2245 tokio::time::timeout(Duration::from_secs(5), started_rx)
2246 .await
2247 .expect("active request did not start")
2248 .expect("active request dropped its start signal");
2249
2250 let queued_ran = Arc::new(AtomicBool::new(false));
2251 let queued_ran_in_op = Arc::clone(&queued_ran);
2252 let queued_top_level = handle
2253 .enqueue_inner(
2254 move |conn| {
2255 queued_ran_in_op.store(true, Ordering::SeqCst);
2256 conn.execute("INSERT INTO t (id, v) VALUES (2, 'queued')", [])
2257 .map_err(|e| StorageError::Pool {
2258 operation: "test_queued_top_level_insert".into(),
2259 message: e.to_string(),
2260 })
2261 },
2262 true,
2263 false,
2264 false,
2265 )
2266 .await
2267 .expect("top-level request must queue behind active request");
2268 release_tx.send(()).expect("release active op");
2269
2270 let active_result = tokio::time::timeout(Duration::from_secs(5), active)
2271 .await
2272 .expect("active caller hung after rollback failure")
2273 .expect("active caller task join");
2274 assert_writer_task_terminal_state(
2275 active_result,
2276 WriterTaskRequestState::SideEffectsUnknown,
2277 );
2278
2279 let queued_result = tokio::time::timeout(Duration::from_secs(5), queued_top_level)
2280 .await
2281 .expect("queued top-level caller hung after terminal failure")
2282 .expect("terminal drain must preserve queued typed reply");
2283 assert_writer_task_terminal_state(queued_result, WriterTaskRequestState::NotStarted);
2284 assert!(
2285 !queued_ran.load(Ordering::SeqCst),
2286 "a top-level request must never run on the poisoned connection"
2287 );
2288
2289 let future_ran = Arc::new(AtomicBool::new(false));
2290 let future_ran_in_op = Arc::clone(&future_ran);
2291 let future_result = handle
2292 .send_top_level(move |_conn| {
2293 future_ran_in_op.store(true, Ordering::SeqCst);
2294 Ok::<(), StorageError>(())
2295 })
2296 .await;
2297 assert_writer_task_terminal_state(future_result, WriterTaskRequestState::NotStarted);
2298 assert!(!future_ran.load(Ordering::SeqCst));
2299 }
2300
2301 #[test]
2302 fn operation_failure_with_failed_rollback_reports_side_effects_unknown() {
2303 let conn = Connection::open_in_memory().expect("in-memory connection");
2304 conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY); BEGIN IMMEDIATE")
2305 .unwrap();
2306 let executions = Arc::new(AtomicUsize::new(0));
2307 let executions_in_op = Arc::clone(&executions);
2308 let (reply_tx, mut reply_rx) = oneshot::channel();
2309 let request = WriteRequest {
2310 op: Box::new(move |conn| -> Result<(), StorageError> {
2311 executions_in_op.fetch_add(1, Ordering::SeqCst);
2312 conn.authorizer(Some(deny_rollback))
2313 .map_err(|e| StorageError::Pool {
2314 operation: "test_install_authorizer".into(),
2315 message: e.to_string(),
2316 })?;
2317 Err(StorageError::Internal(
2318 "intentional operation failure before denied rollback".into(),
2319 ))
2320 }),
2321 reply: reply_tx,
2322 top_level: false,
2323 checkpoint_bypass: false,
2324 vacuum_copy_headroom: false,
2325 telemetry: WriteTelemetry::new(None, "test".to_string(), 0, None),
2326 };
2327
2328 let terminal_state = sealed::Sealed::execute_and_reply_reporting_terminal(
2329 Box::new(request),
2330 &conn,
2331 None,
2332 Duration::ZERO,
2333 Duration::ZERO,
2334 );
2335 assert_eq!(
2336 terminal_state,
2337 Some(WriterTaskRequestState::SideEffectsUnknown)
2338 );
2339 let reply = reply_rx
2340 .try_recv()
2341 .expect("active request must receive a typed terminal reply");
2342 assert_writer_task_terminal_state(reply, WriterTaskRequestState::SideEffectsUnknown);
2343 assert_eq!(executions.load(Ordering::SeqCst), 1);
2344 assert!(
2345 !conn.is_autocommit(),
2346 "the denied ROLLBACK must leave the test connection poisoned"
2347 );
2348 }
2349
2350 #[test]
2351 fn wrapped_panic_with_failed_rollback_reports_side_effects_unknown() {
2352 let conn = Connection::open_in_memory().expect("in-memory connection");
2353 conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY); BEGIN IMMEDIATE")
2354 .unwrap();
2355 let (reply_tx, mut reply_rx) = oneshot::channel();
2356 let request = WriteRequest {
2357 op: Box::new(|conn| -> Result<(), StorageError> {
2358 conn.execute_batch("INSERT INTO t (id) VALUES (1); COMMIT")
2363 .map_err(|e| StorageError::Pool {
2364 operation: "test_force_rollback_failure".into(),
2365 message: e.to_string(),
2366 })?;
2367 panic!("intentional panic after illicit commit");
2368 }),
2369 reply: reply_tx,
2370 top_level: false,
2371 checkpoint_bypass: false,
2372 vacuum_copy_headroom: false,
2373 telemetry: WriteTelemetry::new(None, "test".to_string(), 0, None),
2374 };
2375
2376 let terminal_state = sealed::Sealed::execute_and_reply_reporting_terminal(
2377 Box::new(request),
2378 &conn,
2379 None,
2380 Duration::ZERO,
2381 Duration::ZERO,
2382 );
2383 assert_eq!(
2384 terminal_state,
2385 Some(WriterTaskRequestState::SideEffectsUnknown)
2386 );
2387 let reply = reply_rx
2388 .try_recv()
2389 .expect("active request must receive a typed terminal reply");
2390 assert_writer_task_terminal_state(reply, WriterTaskRequestState::SideEffectsUnknown);
2391
2392 let count: i64 = conn
2393 .query_row("SELECT COUNT(*) FROM t", [], |row| row.get(0))
2394 .unwrap();
2395 assert_eq!(
2396 count, 1,
2397 "the fixture's committed side effect proves why the state must be unknown"
2398 );
2399 }
2400
2401 #[tokio::test]
2405 #[serial(tx_registry)]
2406 async fn wrapped_panic_rolls_back_and_terminally_fails_queue() {
2407 let dir = tempfile::tempdir().unwrap();
2408 let path = dir.path().join("writer_task_wrapped_panic.db");
2409 let cfg = PoolConfig {
2410 path: Some(path),
2411 write_queue_enabled: Some(true),
2412 write_queue_capacity: 8,
2413 ..PoolConfig::for_test()
2414 };
2415 let pool = ConnectionPool::new(cfg).unwrap();
2416 {
2417 let writer = pool.try_writer().unwrap();
2418 writer
2419 .conn()
2420 .execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
2421 .unwrap();
2422 }
2423
2424 let handle = pool
2427 .writer_task_handle()
2428 .expect("writer task lookup")
2429 .expect("file-backed queued pool must spawn its writer task");
2430 assert_eq!(pool.writer_task_spawn_count(), 1);
2431
2432 let (started_tx, started_rx) = oneshot::channel::<()>();
2433 let (release_tx, release_rx) = std_mpsc::channel::<()>();
2434 let active = tokio::spawn({
2435 let handle = handle.clone();
2436 async move {
2437 handle
2438 .send(move |conn| -> Result<usize, StorageError> {
2439 conn.execute("INSERT INTO t (id, v) VALUES (1, 'active')", [])
2440 .map_err(|e| StorageError::Pool {
2441 operation: "test_active_insert".into(),
2442 message: e.to_string(),
2443 })?;
2444 let _ = started_tx.send(());
2445 release_rx.recv().expect("test must release active op");
2446 panic!("intentional wrapped writer request panic");
2447 })
2448 .await
2449 }
2450 });
2451
2452 tokio::time::timeout(Duration::from_secs(5), started_rx)
2453 .await
2454 .expect("active request did not start")
2455 .expect("active request dropped its start signal");
2456
2457 let queued_one_ran = Arc::new(AtomicBool::new(false));
2458 let queued_one_ran_in_op = Arc::clone(&queued_one_ran);
2459 let queued_one = handle
2460 .enqueue(move |conn| {
2461 queued_one_ran_in_op.store(true, Ordering::SeqCst);
2462 conn.execute("INSERT INTO t (id, v) VALUES (2, 'queued-one')", [])
2463 .map_err(|e| StorageError::Pool {
2464 operation: "test_queued_one_insert".into(),
2465 message: e.to_string(),
2466 })
2467 })
2468 .await
2469 .expect("first queued request must be accepted");
2470
2471 let queued_two_ran = Arc::new(AtomicBool::new(false));
2472 let queued_two_ran_in_op = Arc::clone(&queued_two_ran);
2473 let queued_two = handle
2474 .enqueue(move |_conn| {
2475 queued_two_ran_in_op.store(true, Ordering::SeqCst);
2476 Ok::<String, StorageError>("queued-two-ran".to_string())
2477 })
2478 .await
2479 .expect("second queued request must be accepted");
2480
2481 assert_eq!(
2482 handle.queue_depth(),
2483 2,
2484 "both heterogeneous requests must be buffered behind the active op"
2485 );
2486 release_tx.send(()).expect("release active op");
2487
2488 let active_result = tokio::time::timeout(Duration::from_secs(5), active)
2489 .await
2490 .expect("active caller hung after panic")
2491 .expect("active caller task join");
2492 assert_writer_task_terminal_state(
2493 active_result,
2494 WriterTaskRequestState::TransactionRolledBack,
2495 );
2496
2497 let queued_one_result = tokio::time::timeout(Duration::from_secs(5), queued_one)
2498 .await
2499 .expect("first queued caller hung after terminal failure")
2500 .expect("terminal drain must preserve first typed reply");
2501 assert_writer_task_terminal_state(queued_one_result, WriterTaskRequestState::NotStarted);
2502
2503 let queued_two_result = tokio::time::timeout(Duration::from_secs(5), queued_two)
2504 .await
2505 .expect("second queued caller hung after terminal failure")
2506 .expect("terminal drain must preserve second typed reply");
2507 assert_writer_task_terminal_state(queued_two_result, WriterTaskRequestState::NotStarted);
2508 assert!(!queued_one_ran.load(Ordering::SeqCst));
2509 assert!(!queued_two_ran.load(Ordering::SeqCst));
2510
2511 let future_ran = Arc::new(AtomicBool::new(false));
2512 let future_ran_in_op = Arc::clone(&future_ran);
2513 let future_result = handle
2514 .send(move |_conn| {
2515 future_ran_in_op.store(true, Ordering::SeqCst);
2516 Ok::<(), StorageError>(())
2517 })
2518 .await;
2519 assert_writer_task_terminal_state(future_result, WriterTaskRequestState::NotStarted);
2520 assert!(!future_ran.load(Ordering::SeqCst));
2521
2522 let cached_after_failure = pool
2523 .writer_task_handle()
2524 .expect("cached writer task lookup")
2525 .expect("pool retains its terminal handle");
2526 assert_eq!(
2527 pool.writer_task_spawn_count(),
2528 1,
2529 "a terminal writer task must not be restarted behind callers' backs"
2530 );
2531 let cached_result = cached_after_failure
2532 .send(|_conn| Ok::<(), StorageError>(()))
2533 .await;
2534 assert_writer_task_terminal_state(cached_result, WriterTaskRequestState::NotStarted);
2535
2536 let reader = pool.reader().expect("reader");
2537 let count: i64 = reader
2538 .conn()
2539 .query_row("SELECT COUNT(*) FROM t", [], |row| row.get(0))
2540 .unwrap();
2541 assert_eq!(
2542 count, 0,
2543 "the active transaction must be rolled back and queued ops must never run"
2544 );
2545 }
2546
2547 #[tokio::test]
2548 async fn top_level_panic_reports_unknown_and_fails_queue_without_running_it() {
2549 let dir = tempfile::tempdir().unwrap();
2550 let path = dir.path().join("writer_task_top_level_panic.db");
2551 let pool = file_pool(&path);
2552 {
2553 let writer = pool.try_writer().unwrap();
2554 writer
2555 .conn()
2556 .execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
2557 .unwrap();
2558 }
2559 let handle = spawn(&pool, 8).expect("writer task spawn");
2560
2561 let (started_tx, started_rx) = oneshot::channel::<()>();
2562 let (release_tx, release_rx) = std_mpsc::channel::<()>();
2563 let active = tokio::spawn({
2564 let handle = handle.clone();
2565 async move {
2566 handle
2567 .send_top_level(move |conn| -> Result<usize, StorageError> {
2568 conn.execute("INSERT INTO t (id, v) VALUES (10, 'autocommitted')", [])
2569 .map_err(|e| StorageError::Pool {
2570 operation: "test_top_level_insert".into(),
2571 message: e.to_string(),
2572 })?;
2573 let _ = started_tx.send(());
2574 release_rx.recv().expect("test must release top-level op");
2575 panic!("intentional top-level writer request panic");
2576 })
2577 .await
2578 }
2579 });
2580
2581 tokio::time::timeout(Duration::from_secs(5), started_rx)
2582 .await
2583 .expect("top-level request did not start")
2584 .expect("top-level request dropped its start signal");
2585
2586 let queued_ran = Arc::new(AtomicBool::new(false));
2587 let queued_ran_in_op = Arc::clone(&queued_ran);
2588 let queued = handle
2589 .enqueue(move |conn| {
2590 queued_ran_in_op.store(true, Ordering::SeqCst);
2591 conn.execute("INSERT INTO t (id, v) VALUES (11, 'queued')", [])
2592 .map_err(|e| StorageError::Pool {
2593 operation: "test_top_level_queued_insert".into(),
2594 message: e.to_string(),
2595 })
2596 })
2597 .await
2598 .expect("queued request must be accepted");
2599 assert_eq!(handle.queue_depth(), 1);
2600 release_tx.send(()).expect("release top-level op");
2601
2602 let active_result = tokio::time::timeout(Duration::from_secs(5), active)
2603 .await
2604 .expect("top-level caller hung after panic")
2605 .expect("top-level caller task join");
2606 assert_writer_task_terminal_state(
2607 active_result,
2608 WriterTaskRequestState::SideEffectsUnknown,
2609 );
2610
2611 let queued_result = tokio::time::timeout(Duration::from_secs(5), queued)
2612 .await
2613 .expect("queued caller hung after top-level panic")
2614 .expect("terminal drain must preserve queued typed reply");
2615 assert_writer_task_terminal_state(queued_result, WriterTaskRequestState::NotStarted);
2616 assert!(!queued_ran.load(Ordering::SeqCst));
2617
2618 let reader = pool.reader().expect("reader");
2619 let active_count: i64 = reader
2620 .conn()
2621 .query_row("SELECT COUNT(*) FROM t WHERE id = 10", [], |row| row.get(0))
2622 .unwrap();
2623 let queued_count: i64 = reader
2624 .conn()
2625 .query_row("SELECT COUNT(*) FROM t WHERE id = 11", [], |row| row.get(0))
2626 .unwrap();
2627 assert_eq!(
2628 active_count, 1,
2629 "the completed top-level statement autocommits before the panic"
2630 );
2631 assert_eq!(queued_count, 0, "the queued request must never run");
2632 }
2633
2634 #[tokio::test]
2635 async fn closed_receiver_rejects_all_send_surfaces_as_not_started() {
2636 let (tx, rx) = mpsc::channel::<Box<dyn AnyWriteRequest + Send>>(4);
2640 drop(rx);
2641
2642 let handle = WriterTaskHandle {
2643 tx,
2644 backend_key: None,
2645 db: "test".to_string(),
2646 slow_write_threshold: None,
2647 enqueue_timeout: Duration::from_secs(5),
2648 };
2649 let send_result = handle.send(|_conn| Ok::<(), StorageError>(())).await;
2650 assert_writer_task_terminal_state(send_result, WriterTaskRequestState::NotStarted);
2651
2652 let timed_result = handle
2653 .send_with_timeout(|_conn| Ok::<(), StorageError>(()), Duration::from_secs(1))
2654 .await;
2655 assert_writer_task_terminal_state(timed_result, WriterTaskRequestState::NotStarted);
2656
2657 let top_level_result = handle
2658 .send_top_level(|_conn| Ok::<(), StorageError>(()))
2659 .await;
2660 assert_writer_task_terminal_state(top_level_result, WriterTaskRequestState::NotStarted);
2661 }
2662
2663 #[tokio::test]
2664 async fn accepted_request_lost_reply_is_side_effects_unknown() {
2665 let (tx, mut rx) = mpsc::channel::<Box<dyn AnyWriteRequest + Send>>(1);
2669 let handle = WriterTaskHandle {
2670 tx,
2671 backend_key: None,
2672 db: "test".to_string(),
2673 slow_write_threshold: None,
2674 enqueue_timeout: Duration::from_secs(5),
2675 };
2676 let request_ran = Arc::new(AtomicBool::new(false));
2677 let request_ran_in_op = Arc::clone(&request_ran);
2678
2679 let dropper = tokio::spawn(async move {
2680 let request = rx.recv().await.expect("request must be accepted");
2681 drop(request);
2682 });
2683 let result = tokio::time::timeout(
2684 Duration::from_secs(5),
2685 handle.send(move |_conn| {
2686 request_ran_in_op.store(true, Ordering::SeqCst);
2687 Ok::<(), StorageError>(())
2688 }),
2689 )
2690 .await
2691 .expect("caller hung after accepted request was dropped");
2692 dropper.await.expect("dropper task join");
2693
2694 assert_writer_task_terminal_state(result, WriterTaskRequestState::SideEffectsUnknown);
2695 assert!(!request_ran.load(Ordering::SeqCst));
2696 }
2697
2698 #[cfg(unix)]
2701 #[test]
2702 fn writer_stage_backend_key_preserves_non_utf8_path_bytes() {
2703 use std::ffi::OsString;
2704 use std::os::unix::ffi::OsStringExt;
2705
2706 let path_a =
2707 std::path::PathBuf::from(OsString::from_vec(b"/tmp/khive-writer-\x80.db".to_vec()));
2708 let path_b =
2709 std::path::PathBuf::from(OsString::from_vec(b"/tmp/khive-writer-\x81.db".to_vec()));
2710 assert_eq!(
2711 path_a.display().to_string(),
2712 path_b.display().to_string(),
2713 "fixture must reproduce the lossy display-label collision"
2714 );
2715 assert_ne!(
2716 writer_db_key_from_path(Some(&path_a)),
2717 writer_db_key_from_path(Some(&path_b)),
2718 "backend keys must retain the canonical path's exact OS bytes"
2719 );
2720 }
2721
2722 #[tokio::test]
2726 async fn writer_stage_sample_attributes_a_slow_body() {
2727 let dir = tempfile::tempdir().unwrap();
2728 let path = dir.path().join("writer_stage_sample.db");
2729 let pool = ConnectionPool::new(PoolConfig {
2732 path: Some(path.clone()),
2733 volume_lock_dir: Some(dir.path().join("volume-locks")),
2734 ..PoolConfig::for_test()
2735 })
2736 .expect("pool open");
2737 {
2738 let writer = pool.try_writer().unwrap();
2739 writer
2740 .conn()
2741 .execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY)")
2742 .unwrap();
2743 }
2744 let handle = spawn(&pool, 8).unwrap();
2745
2746 handle
2750 .send(|conn| {
2751 std::thread::sleep(Duration::from_millis(400));
2752 conn.execute("INSERT INTO t VALUES (1)", [])
2753 .map_err(|error| StorageError::Pool {
2754 operation: "writer_stage_sample".into(),
2755 message: error.to_string(),
2756 })
2757 })
2758 .await
2759 .unwrap();
2760
2761 let sample = last_writer_stage_observation(&pool).expect("writer stage sample");
2762 assert!(
2763 sample.body_micros >= 350_000,
2764 "the synthetic delay must land in the body stage: {sample:?}"
2765 );
2766 assert!(
2767 sample.body_micros > sample.queue_wait_micros,
2768 "fast queueing must not receive the body's delay: {sample:?}"
2769 );
2770 assert!(
2771 sample.body_micros > sample.transaction_acquire_micros,
2772 "an uncontended BEGIN must not receive the body's delay: {sample:?}"
2773 );
2774 assert!(
2775 sample.body_micros > sample.commit_micros,
2776 "a fast COMMIT must not receive the body's delay: {sample:?}"
2777 );
2778 assert!(sample.observed_at_unix_ms > 0);
2779 }
2780
2781 #[test]
2786 fn writer_queue_wait_excludes_blocking_pool_scheduling_delay() {
2787 let runtime = tokio::runtime::Builder::new_multi_thread()
2788 .worker_threads(1)
2789 .max_blocking_threads(1)
2790 .enable_all()
2791 .build()
2792 .expect("test runtime");
2793
2794 runtime.block_on(async {
2795 let dir = tempfile::tempdir().unwrap();
2796 let path = dir.path().join("writer_dequeue_boundary.db");
2797 let pool = file_pool(&path);
2798 let handle = spawn(&pool, 8).unwrap();
2799
2800 let (blocker_started_tx, blocker_started_rx) = std_mpsc::sync_channel(0);
2801 let (release_blocker_tx, release_blocker_rx) = std_mpsc::channel();
2802 let blocker = tokio::task::spawn_blocking(move || {
2803 blocker_started_tx.send(()).unwrap();
2804 release_blocker_rx.recv().unwrap();
2805 });
2806 blocker_started_rx
2807 .recv_timeout(Duration::from_secs(1))
2808 .expect("sole blocking worker must be occupied");
2809
2810 let reply = handle
2811 .enqueue(|_conn| Ok::<(), StorageError>(()))
2812 .await
2813 .expect("request must enter the bounded writer channel");
2814 let dequeue_deadline = Instant::now() + Duration::from_secs(1);
2815 while handle.queue_depth() != 0 {
2816 assert!(
2817 Instant::now() < dequeue_deadline,
2818 "writer drain never dequeued the accepted request"
2819 );
2820 tokio::task::yield_now().await;
2821 }
2822
2823 let scheduling_delay = Duration::from_millis(150);
2824 tokio::time::sleep(scheduling_delay).await;
2825 release_blocker_tx.send(()).unwrap();
2826 blocker.await.unwrap();
2827 reply.await.unwrap().unwrap();
2828
2829 let sample = last_writer_stage_observation(&pool).expect("writer stage sample");
2830 assert!(
2831 sample.total_micros.saturating_sub(sample.queue_wait_micros) >= 100_000,
2832 "the post-dequeue blocking-pool delay must not inflate queue_wait: {sample:?}"
2833 );
2834 });
2835 }
2836
2837 #[tokio::test]
2841 #[serial(tx_registry)]
2842 async fn writer_task_failure_counters_are_acquisition_site_exact() {
2843 {
2849 let dir = tempfile::tempdir().unwrap();
2850 let path = dir.path().join("writer_task_failure_counters_rollback.db");
2851 let pool = file_pool(&path);
2852 let handle = spawn(&pool, 8).expect("writer task should spawn");
2853
2854 let before = pool.writer_acquisition_snapshot();
2855 assert_eq!(before.writer_task_request_failures, 0);
2856 assert_eq!(before.writer_task_side_effects_unknown, 0);
2857
2858 let (started_tx, started_rx) = oneshot::channel::<()>();
2859 let (release_tx, release_rx) = std_mpsc::channel::<()>();
2860 let active = tokio::spawn({
2861 let handle = handle.clone();
2862 async move {
2863 handle
2864 .send(move |_conn| -> Result<(), StorageError> {
2865 let _ = started_tx.send(());
2866 release_rx.recv().expect("test must release active op");
2867 panic!("intentional rollback-clean panic for counter test");
2868 })
2869 .await
2870 }
2871 });
2872 tokio::time::timeout(Duration::from_secs(5), started_rx)
2873 .await
2874 .expect("active request did not start")
2875 .expect("active request dropped its start signal");
2876
2877 let queued = handle
2878 .enqueue(|_conn| Ok::<(), StorageError>(()))
2879 .await
2880 .expect("second request must queue behind the active one");
2881
2882 release_tx.send(()).expect("release active op");
2883 let active_result = tokio::time::timeout(Duration::from_secs(5), active)
2884 .await
2885 .expect("active caller hung after panic")
2886 .expect("active caller task join");
2887 assert_writer_task_terminal_state(
2888 active_result,
2889 WriterTaskRequestState::TransactionRolledBack,
2890 );
2891
2892 let queued_result = queued.await.expect("terminal drain must reply");
2893 assert_writer_task_terminal_state(queued_result, WriterTaskRequestState::NotStarted);
2894
2895 let after = pool.writer_acquisition_snapshot();
2896 assert_eq!(
2897 after.writer_task_request_failures, 1,
2898 "only the request that actually reached the seam counts, not the ones \
2899 failed by the queue-close drain"
2900 );
2901 assert_eq!(
2902 after.writer_task_side_effects_unknown, 0,
2903 "a clean rollback must not be counted as an unknown-side-effects outcome"
2904 );
2905 }
2906
2907 {
2911 let dir = tempfile::tempdir().unwrap();
2912 let path = dir.path().join("writer_task_failure_counters_unknown.db");
2913 let pool = file_pool(&path);
2914 let handle = spawn(&pool, 8).expect("writer task should spawn");
2915
2916 let before = pool.writer_acquisition_snapshot();
2917
2918 let active_result = handle
2919 .send(|conn| -> Result<(), StorageError> {
2920 conn.authorizer(Some(deny_rollback))
2921 .map_err(|e| StorageError::Pool {
2922 operation: "test_install_authorizer".into(),
2923 message: e.to_string(),
2924 })?;
2925 Err(StorageError::Internal(
2926 "intentional operation failure before denied rollback".into(),
2927 ))
2928 })
2929 .await;
2930 assert_writer_task_terminal_state(
2931 active_result,
2932 WriterTaskRequestState::SideEffectsUnknown,
2933 );
2934
2935 let sentinel_result = handle.send(|_conn| Ok::<(), StorageError>(())).await;
2942 assert_writer_task_terminal_state(sentinel_result, WriterTaskRequestState::NotStarted);
2943
2944 let after = pool.writer_acquisition_snapshot();
2945 assert_eq!(
2946 after.writer_task_request_failures - before.writer_task_request_failures,
2947 1
2948 );
2949 assert_eq!(
2950 after.writer_task_side_effects_unknown - before.writer_task_side_effects_unknown,
2951 1
2952 );
2953 }
2954 }
2955}
2956
2957#[cfg(test)]
2958#[path = "writer_task_lease_close_tests.rs"]
2959mod volume_lease_close_tests;
2960
2961#[cfg(all(test, any(unix, windows)))]
2962#[path = "writer_task_identity_tests.rs"]
2963mod identity_tests;