1use crate::database::{Database, ExternalTriggerBridge, TableHandle};
31use crate::epoch::{Epoch, Snapshot};
32use crate::error::{MongrelError, Result};
33use crate::memtable::Value;
34use crate::rowid::RowId;
35use crate::wal::SharedWal;
36use mongreldb_types::hlc::HlcTimestamp;
37use parking_lot::{Condvar, Mutex as PlMutex};
38use std::sync::Arc;
39
40pub(crate) fn allocate_txn_id(allocator: &PlMutex<u64>) -> Result<u64> {
41 let mut next = allocator.lock();
42 let id = *next;
43 if id == crate::wal::SYSTEM_TXN_ID || id & u32::MAX as u64 == 0 {
44 return Err(MongrelError::Full(
45 "per-open transaction id namespace exhausted; reopen the database".into(),
46 ));
47 }
48 *next = id.checked_add(1).ok_or_else(|| {
49 MongrelError::Full(
50 "per-open transaction id namespace exhausted; reopen the database".into(),
51 )
52 })?;
53 Ok(id)
54}
55
56pub(crate) enum Staged {
58 Put(Vec<(u16, Value)>),
59 Delete(RowId),
60 Update {
64 row_id: RowId,
65 new_row: Vec<(u16, Value)>,
66 changed_columns: Vec<u16>,
67 },
68 Truncate,
69}
70
71#[derive(Debug, Clone)]
72pub struct OwnedRow {
73 pub columns: Vec<(u16, Value)>,
74}
75
76#[derive(Debug, Clone)]
77pub struct PutResult {
78 pub auto_inc: Option<i64>,
79 pub row: OwnedRow,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum UpsertActionKind {
84 Inserted,
85 Updated,
86 Unchanged,
87}
88
89#[derive(Debug, Clone)]
90pub enum UpsertAction {
91 DoNothing,
92 DoUpdate(Vec<(u16, Value)>),
93}
94
95#[derive(Debug, Clone)]
96pub struct UpsertResult {
97 pub action: UpsertActionKind,
98 pub row: OwnedRow,
99 pub auto_inc: Option<i64>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum AbortReason {
107 RolledBack,
109 Conflict(String),
112 Validation(String),
115 Cancelled(String),
117 Error(String),
119}
120
121#[derive(Debug, Clone)]
128pub enum TransactionState {
129 Active,
131 Preparing,
134 CommitCritical,
137 Committed(mongreldb_log::CommitReceipt),
139 Aborted(AbortReason),
141}
142
143#[derive(Debug, Clone)]
151pub struct TxnStateHandle {
152 inner: std::sync::Arc<PlMutex<TransactionState>>,
153}
154
155impl TxnStateHandle {
156 fn new() -> Self {
157 Self {
158 inner: std::sync::Arc::new(PlMutex::new(TransactionState::Active)),
159 }
160 }
161
162 pub fn state(&self) -> TransactionState {
164 self.inner.lock().clone()
165 }
166
167 fn transition(&self, next: TransactionState) -> bool {
170 let mut current = self.inner.lock();
171 let legal = matches!(
172 (&*current, &next),
173 (TransactionState::Active, TransactionState::Preparing)
174 | (TransactionState::Active, TransactionState::Aborted(_))
175 | (
176 TransactionState::Preparing,
177 TransactionState::CommitCritical
178 )
179 | (TransactionState::Preparing, TransactionState::Aborted(_))
180 | (TransactionState::Preparing, TransactionState::Committed(_))
183 | (
184 TransactionState::CommitCritical,
185 TransactionState::Committed(_)
186 )
187 );
188 if legal {
189 *current = next;
190 }
191 legal
192 }
193
194 pub(crate) fn begin_prepare(&self) -> bool {
195 self.transition(TransactionState::Preparing)
196 }
197
198 pub(crate) fn enter_commit_critical(&self) -> bool {
199 self.transition(TransactionState::CommitCritical)
200 }
201
202 pub(crate) fn committed(&self, receipt: mongreldb_log::CommitReceipt) -> bool {
203 self.transition(TransactionState::Committed(receipt))
204 }
205
206 pub fn abort(&self, reason: AbortReason) -> bool {
209 self.transition(TransactionState::Aborted(reason))
210 }
211}
212
213pub(crate) fn classify_commit_error(state: &TxnStateHandle, error: &MongrelError) {
216 let reason = match error {
217 MongrelError::Conflict(message) => AbortReason::Conflict(message.clone()),
218 MongrelError::SerializationFailure { message } => {
223 AbortReason::Conflict(format!("serialization failure: {message}"))
224 }
225 MongrelError::Cancelled => AbortReason::Cancelled("cancelled".into()),
226 MongrelError::DeadlineExceeded => AbortReason::Cancelled("deadline exceeded".into()),
227 MongrelError::CommitOutcomeUnknown { .. } | MongrelError::DurableCommit { .. } => return,
229 MongrelError::InvalidArgument(message)
230 | MongrelError::Schema(message)
231 | MongrelError::TriggerValidation(message) => AbortReason::Validation(message.clone()),
232 other => AbortReason::Error(other.to_string()),
233 };
234 state.abort(reason);
235}
236
237#[derive(Debug, Clone, Default)]
243pub struct ReadSet {
244 rows: std::collections::BTreeSet<(u64, u64)>,
245}
246
247impl ReadSet {
248 pub(crate) fn record_row(&mut self, table_id: u64, row_id: RowId) {
249 self.rows.insert((table_id, row_id.0));
250 }
251
252 pub fn rows(&self) -> impl Iterator<Item = (u64, u64)> + '_ {
254 self.rows.iter().copied()
255 }
256
257 pub fn len(&self) -> usize {
258 self.rows.len()
259 }
260
261 pub fn is_empty(&self) -> bool {
262 self.rows.is_empty()
263 }
264}
265
266#[derive(Debug, Clone, Default)]
273pub struct PredicateSet {
274 tables: std::collections::BTreeSet<u64>,
275}
276
277impl PredicateSet {
278 pub(crate) fn record_table(&mut self, table_id: u64) {
279 self.tables.insert(table_id);
280 }
281
282 pub fn tables(&self) -> impl Iterator<Item = u64> + '_ {
284 self.tables.iter().copied()
285 }
286
287 pub fn len(&self) -> usize {
288 self.tables.len()
289 }
290
291 pub fn is_empty(&self) -> bool {
292 self.tables.is_empty()
293 }
294}
295
296pub(crate) fn ssi_validation_keys(
300 read_set: &ReadSet,
301 predicate_set: &PredicateSet,
302) -> Vec<WriteKey> {
303 let mut keys = Vec::with_capacity(read_set.len() + predicate_set.len());
304 for (table_id, row_id) in read_set.rows() {
305 keys.push(WriteKey::Row { table_id, row_id });
306 }
307 for table_id in predicate_set.tables() {
308 keys.push(WriteKey::Table { table_id });
309 }
310 keys
311}
312
313pub(crate) struct TxnCommitContext {
318 pub isolation: IsolationLevel,
320 pub read_ts: Option<HlcTimestamp>,
324 pub read_set: ReadSet,
326 pub predicate_set: PredicateSet,
328 pub state: Option<TxnStateHandle>,
330 pub idempotency: Option<IdempotencyRequest>,
332}
333
334impl TxnCommitContext {
335 pub(crate) fn internal() -> Self {
339 Self {
340 isolation: IsolationLevel::RepeatableRead,
341 read_ts: None,
342 read_set: ReadSet::default(),
343 predicate_set: PredicateSet::default(),
344 state: None,
345 idempotency: None,
346 }
347 }
348}
349
350pub struct Transaction<'db> {
357 db: &'db Database,
358 txn_id: u64,
359 allocation_error: Option<String>,
360 isolation: IsolationLevel,
361 read: Snapshot,
362 read_ts: Option<HlcTimestamp>,
363 read_set: ReadSet,
364 predicate_set: PredicateSet,
365 state: TxnStateHandle,
366 staging: Vec<(u64 , Staged)>,
367 external_states: Vec<(String, Vec<u8>)>,
368 materialized_view_updates: Vec<crate::catalog::MaterializedViewEntry>,
369 principal: Option<crate::auth::Principal>,
370 principal_catalog_bound: bool,
371 external_trigger_bridge: Option<&'db dyn ExternalTriggerBridge>,
372 _active: Option<ActiveTxnGuard<'db>>,
373}
374
375impl<'db> Transaction<'db> {
376 pub(crate) fn new(
377 db: &'db Database,
378 txn_id: Result<u64>,
379 read: Snapshot,
380 isolation: IsolationLevel,
381 ) -> Self {
382 let guard = db.register_active(read.epoch);
383 let read_ts = db.hlc_clock().now().ok();
387 let (txn_id, allocation_error) = match txn_id {
388 Ok(txn_id) => (txn_id, None),
389 Err(MongrelError::Full(message)) => (crate::wal::SYSTEM_TXN_ID, Some(message)),
390 Err(error) => (crate::wal::SYSTEM_TXN_ID, Some(error.to_string())),
391 };
392 Self {
393 db,
394 txn_id,
395 allocation_error,
396 isolation,
397 read,
398 read_ts,
399 read_set: ReadSet::default(),
400 predicate_set: PredicateSet::default(),
401 state: TxnStateHandle::new(),
402 staging: Vec::new(),
403 external_states: Vec::new(),
404 materialized_view_updates: Vec::new(),
405 principal: None,
406 principal_catalog_bound: false,
407 external_trigger_bridge: None,
408 _active: Some(guard),
409 }
410 }
411
412 pub(crate) fn with_external_trigger_bridge(
413 mut self,
414 bridge: &'db dyn ExternalTriggerBridge,
415 ) -> Self {
416 self.external_trigger_bridge = Some(bridge);
417 self
418 }
419
420 pub(crate) fn with_principal(
421 mut self,
422 principal: Option<crate::auth::Principal>,
423 catalog_bound: bool,
424 ) -> Self {
425 self.principal = principal;
426 self.principal_catalog_bound = catalog_bound;
427 self
428 }
429
430 pub fn read_snapshot(&self) -> Snapshot {
431 self.read
432 }
433
434 pub fn txn_id(&self) -> u64 {
437 self.txn_id
438 }
439
440 pub fn isolation(&self) -> IsolationLevel {
442 self.isolation
443 }
444
445 pub fn read_ts(&self) -> Option<HlcTimestamp> {
448 self.read_ts
449 }
450
451 pub fn state(&self) -> TransactionState {
453 self.state.state()
454 }
455
456 pub fn state_handle(&self) -> TxnStateHandle {
459 self.state.clone()
460 }
461
462 pub fn read_set(&self) -> &ReadSet {
464 &self.read_set
465 }
466
467 pub fn predicate_set(&self) -> &PredicateSet {
469 &self.predicate_set
470 }
471
472 fn statement_snapshot(&self) -> Snapshot {
476 match self.isolation.canonical() {
477 IsolationLevel::ReadCommitted => self.db.visible_snapshot(),
478 _ => self.read,
479 }
480 }
481
482 pub fn get(&mut self, table: &str, row_id: RowId) -> Result<Option<OwnedRow>> {
487 let snap = self.statement_snapshot();
488 let id = self.db.table_id(table)?;
489 let handle = self.db.table(table)?;
490 let row = handle.read().get(row_id, snap);
491 Ok(row.map(|row| {
492 self.read_set.record_row(id, row_id);
493 owned_row_from_map(row.columns)
494 }))
495 }
496
497 pub fn track_predicate_read(&mut self, table: &str) -> Result<()> {
501 let id = self.db.table_id(table)?;
502 self.predicate_set.record_table(id);
503 Ok(())
504 }
505
506 fn lock_auto_inc_barrier(&self, table_id: u64, cells: &[(u16, Value)]) -> Result<()> {
521 if self.db.table_auto_inc_would_allocate(table_id, cells) {
522 self.db.acquire_txn_lock(
523 self.txn_id,
524 crate::locks::LockKey::sequence_barrier(format!("auto_inc:{table_id}").as_str()),
525 crate::locks::LockMode::Exclusive,
526 None,
527 )?;
528 }
529 Ok(())
530 }
531
532 pub fn put(&mut self, table: &str, mut cells: Vec<(u16, Value)>) -> Result<Option<i64>> {
533 self.require_columns(table, crate::auth::ColumnOperation::Insert, &cells)?;
534 let id = self.db.table_id(table)?;
535 self.lock_auto_inc_barrier(id, &cells)?;
536 let handle = self.db.table(table)?;
537 let mut t = handle.lock();
538 let assigned = t.fill_auto_inc(&mut cells)?;
539 t.apply_defaults(&mut cells)?;
540 drop(t);
541 self.staging.push((id, Staged::Put(cells)));
542 Ok(assigned)
543 }
544
545 #[doc(hidden)]
547 pub fn put_building(
548 &mut self,
549 table: &str,
550 mut cells: Vec<(u16, Value)>,
551 ) -> Result<Option<i64>> {
552 self.db
553 .require_for(self.principal.as_ref(), &crate::auth::Permission::Ddl)?;
554 let id = self.db.building_table_id(table)?;
555 self.lock_auto_inc_barrier(id, &cells)?;
556 let handle = self.db.table_by_id(id)?;
557 let mut target = handle.lock();
558 let assigned = target.fill_auto_inc(&mut cells)?;
559 target.apply_defaults(&mut cells)?;
560 let primary_key_column = target
561 .schema()
562 .primary_key()
563 .map(|column| column.id)
564 .ok_or_else(|| MongrelError::Schema("CTAS build table has no primary key".into()))?;
565 let primary_key = cells
566 .iter()
567 .find(|(column, _)| *column == primary_key_column)
568 .map(|(_, value)| value)
569 .ok_or_else(|| MongrelError::InvalidArgument("CTAS primary key is missing".into()))?;
570 if matches!(primary_key, Value::Null) {
571 return Err(MongrelError::InvalidArgument(
572 "CTAS primary key cannot be NULL".into(),
573 ));
574 }
575 let primary_key = primary_key.encode_key();
576 let replacing = self
577 .staging
578 .iter()
579 .any(|(table_id, staged)| *table_id == id && matches!(staged, Staged::Truncate));
580 if !replacing && target.lookup_pk(&primary_key).is_some() {
581 return Err(MongrelError::InvalidArgument(
582 "duplicate CTAS primary key".into(),
583 ));
584 }
585 drop(target);
586 if self.staging.iter().any(|(staged_table, staged)| {
587 if *staged_table != id {
588 return false;
589 }
590 let Staged::Put(staged_cells) = staged else {
591 return false;
592 };
593 staged_cells
594 .iter()
595 .find(|(column, _)| *column == primary_key_column)
596 .is_some_and(|(_, value)| value.encode_key() == primary_key)
597 }) {
598 return Err(MongrelError::InvalidArgument(
599 "duplicate CTAS primary key".into(),
600 ));
601 }
602 self.staging.push((id, Staged::Put(cells)));
603 Ok(assigned)
604 }
605
606 #[doc(hidden)]
608 pub fn truncate_building(&mut self, table: &str) -> Result<()> {
609 self.db
610 .require_for(self.principal.as_ref(), &crate::auth::Permission::Ddl)?;
611 let id = self.db.building_table_id(table)?;
612 if self.staging.iter().any(|(table_id, _)| *table_id == id) {
613 return Err(MongrelError::InvalidArgument(
614 "building-table truncate must be staged before replacement rows".into(),
615 ));
616 }
617 self.staging.push((id, Staged::Truncate));
618 Ok(())
619 }
620
621 pub fn put_returning(
622 &mut self,
623 table: &str,
624 mut cells: Vec<(u16, Value)>,
625 ) -> Result<PutResult> {
626 self.require_columns(table, crate::auth::ColumnOperation::Insert, &cells)?;
627 let id = self.db.table_id(table)?;
628 self.lock_auto_inc_barrier(id, &cells)?;
629 let handle = self.db.table(table)?;
630 let mut t = handle.lock();
631 let assigned = t.fill_auto_inc(&mut cells)?;
632 t.apply_defaults(&mut cells)?;
633 drop(t);
634 let row = owned_row_from_cells(&cells);
635 self.staging.push((id, Staged::Put(cells)));
636 Ok(PutResult {
637 auto_inc: assigned,
638 row,
639 })
640 }
641
642 #[doc(hidden)]
645 pub fn put_returning_bound(
646 &mut self,
647 table: &str,
648 expected_table_id: u64,
649 expected_schema_id: u64,
650 mut cells: Vec<(u16, Value)>,
651 ) -> Result<PutResult> {
652 self.require_columns(table, crate::auth::ColumnOperation::Insert, &cells)?;
653 self.lock_auto_inc_barrier(expected_table_id, &cells)?;
654 let handle = self.bound_table(table, expected_table_id, expected_schema_id)?;
655 let mut target = handle.lock();
656 let assigned = target.fill_auto_inc(&mut cells)?;
657 target.apply_defaults(&mut cells)?;
658 drop(target);
659 let row = owned_row_from_cells(&cells);
660 self.staging.push((expected_table_id, Staged::Put(cells)));
661 Ok(PutResult {
662 auto_inc: assigned,
663 row,
664 })
665 }
666
667 pub fn put_batch(
673 &mut self,
674 table: &str,
675 rows: Vec<Vec<(u16, Value)>>,
676 ) -> Result<Vec<Option<i64>>> {
677 if !rows.is_empty() {
678 let mut columns = rows
679 .iter()
680 .flat_map(|cells| cells.iter().map(|(column, _)| *column))
681 .collect::<Vec<_>>();
682 columns.sort_unstable();
683 columns.dedup();
684 self.db.require_columns_for(
685 table,
686 crate::auth::ColumnOperation::Insert,
687 &columns,
688 self.principal.as_ref(),
689 )?;
690 }
691 let id = self.db.table_id(table)?;
692 for cells in &rows {
693 self.lock_auto_inc_barrier(id, cells)?;
694 }
695 let handle = self.db.table(table)?;
696 let mut t = handle.lock();
697 let mut assigned = Vec::with_capacity(rows.len());
698 for mut cells in rows {
699 let a = t.fill_auto_inc(&mut cells)?;
700 t.apply_defaults(&mut cells)?;
701 assigned.push(a);
702 self.staging.push((id, Staged::Put(cells)));
703 }
704 drop(t);
705 Ok(assigned)
706 }
707
708 pub fn delete(&mut self, table: &str, row_id: RowId) -> Result<()> {
710 self.delete_batch(table, vec![row_id])
711 }
712
713 #[doc(hidden)]
716 pub fn delete_bound(
717 &mut self,
718 table: &str,
719 expected_table_id: u64,
720 expected_schema_id: u64,
721 row_id: RowId,
722 ) -> Result<()> {
723 self.require_delete(table)?;
724 self.bound_table(table, expected_table_id, expected_schema_id)?;
725 self.reject_after_truncate(expected_table_id)?;
726 self.staging
727 .push((expected_table_id, Staged::Delete(row_id)));
728 Ok(())
729 }
730
731 #[doc(hidden)]
734 pub fn delete_by_pk_bound(
735 &mut self,
736 table: &str,
737 expected_table_id: u64,
738 expected_schema_id: u64,
739 key: &Value,
740 ) -> Result<bool> {
741 self.require_delete(table)?;
742 let handle = self.bound_table(table, expected_table_id, expected_schema_id)?;
743 self.reject_after_truncate(expected_table_id)?;
744 let row_id = {
745 let mut target = handle.lock();
746 target.ensure_indexes_complete()?;
747 target.lookup_pk(&key.encode_key())
748 };
749 let Some(row_id) = row_id else {
750 return Ok(false);
751 };
752 self.read_set.record_row(expected_table_id, row_id);
753 self.staging
754 .push((expected_table_id, Staged::Delete(row_id)));
755 Ok(true)
756 }
757
758 pub fn delete_batch(&mut self, table: &str, row_ids: Vec<RowId>) -> Result<()> {
760 self.require_delete(table)?;
761 let id = self.db.table_id(table)?;
762 self.reject_after_truncate(id)?;
763 self.staging.extend(
764 row_ids
765 .into_iter()
766 .map(|row_id| (id, Staged::Delete(row_id))),
767 );
768 Ok(())
769 }
770
771 pub fn put_external_state(&mut self, table: &str, state: Vec<u8>) -> Result<()> {
774 if self.db.external_table(table).is_none() {
775 return Err(MongrelError::NotFound(format!(
776 "external table {table:?} not found"
777 )));
778 }
779 self.external_states.push((table.to_string(), state));
780 Ok(())
781 }
782
783 pub fn set_materialized_view_definition(
787 &mut self,
788 definition: crate::catalog::MaterializedViewEntry,
789 ) -> Result<()> {
790 self.db
791 .require_for(self.principal.as_ref(), &crate::auth::Permission::Ddl)?;
792 if self.db.table_id(&definition.name).is_err() {
793 return Err(MongrelError::NotFound(format!(
794 "materialized view table {:?} not found",
795 definition.name
796 )));
797 }
798 self.materialized_view_updates
799 .retain(|current| current.name != definition.name);
800 self.materialized_view_updates.push(definition);
801 Ok(())
802 }
803
804 pub fn delete_many(&mut self, table: &str, row_ids: Vec<RowId>) -> Result<Vec<OwnedRow>> {
805 self.require_delete(table)?;
806 let id = self.db.table_id(table)?;
807 self.reject_after_truncate(id)?;
808 let snap = self.statement_snapshot();
809 let handle = self.db.table(table)?;
810 let t = handle.lock();
811 let mut pre_images = Vec::with_capacity(row_ids.len());
812 for row_id in &row_ids {
813 if let Some(row) = t.get(*row_id, snap) {
814 pre_images.push(owned_row_from_map(row.columns));
815 self.read_set.record_row(id, *row_id);
816 }
817 }
818 drop(t);
819 for row_id in row_ids {
820 self.staging.push((id, Staged::Delete(row_id)));
821 }
822 Ok(pre_images)
823 }
824
825 pub fn update_many(
826 &mut self,
827 table: &str,
828 updates: Vec<(RowId, Vec<(u16, Value)>)>,
829 ) -> Result<Vec<OwnedRow>> {
830 if !updates.is_empty() {
831 let mut columns = updates
832 .iter()
833 .flat_map(|(_, cells)| cells.iter().map(|(column, _)| *column))
834 .collect::<Vec<_>>();
835 columns.sort_unstable();
836 columns.dedup();
837 self.db.require_columns_for(
838 table,
839 crate::auth::ColumnOperation::Update,
840 &columns,
841 self.principal.as_ref(),
842 )?;
843 }
844 let id = self.db.table_id(table)?;
845 self.reject_after_truncate(id)?;
846 let snap = self.statement_snapshot();
847 let handle = self.db.table(table)?;
848 let t = handle.lock();
849 let mut post_images = Vec::with_capacity(updates.len());
850 let mut staged = Vec::with_capacity(updates.len());
851 for (old_id, new_cells) in updates {
852 let changed_columns = changed_columns(&new_cells);
853 let old_row = t
854 .get(old_id, snap)
855 .ok_or_else(|| MongrelError::NotFound(format!("row {old_id:?} not found")))?;
856 self.read_set.record_row(id, old_id);
857 let merged = merge_cells(old_row.columns.into_iter().collect(), new_cells);
858 post_images.push(owned_row_from_cells(&merged));
859 staged.push((
860 id,
861 Staged::Update {
862 row_id: old_id,
863 new_row: merged,
864 changed_columns,
865 },
866 ));
867 }
868 drop(t);
869 self.staging.extend(staged);
870 Ok(post_images)
871 }
872
873 pub fn upsert(
874 &mut self,
875 table: &str,
876 mut insert_cells: Vec<(u16, Value)>,
877 action: UpsertAction,
878 ) -> Result<UpsertResult> {
879 self.require_columns(table, crate::auth::ColumnOperation::Insert, &insert_cells)?;
883 let id = self.db.table_id(table)?;
884 self.lock_auto_inc_barrier(id, &insert_cells)?;
885 self.reject_after_truncate(id)?;
886 match (self.existing_pk_row(table, &insert_cells)?, action) {
887 (None, _) => {
888 let handle = self.db.table(table)?;
889 let mut t = handle.lock();
890 let assigned = t.fill_auto_inc(&mut insert_cells)?;
891 t.apply_defaults(&mut insert_cells)?;
892 drop(t);
893 let row = owned_row_from_cells(&insert_cells);
894 self.staging.push((id, Staged::Put(insert_cells)));
895 Ok(UpsertResult {
896 action: UpsertActionKind::Inserted,
897 row,
898 auto_inc: assigned,
899 })
900 }
901 (Some((_old_id, old_row)), UpsertAction::DoNothing) => Ok(UpsertResult {
902 action: UpsertActionKind::Unchanged,
903 row: old_row,
904 auto_inc: None,
905 }),
906 (Some((old_id, old_row)), UpsertAction::DoUpdate(update_cells)) => {
907 self.require_columns(table, crate::auth::ColumnOperation::Update, &update_cells)?;
909 let changed_columns = changed_columns(&update_cells);
910 let merged = merge_cells(old_row.columns.clone(), update_cells);
911 if columns_equal(&old_row.columns, &merged) {
912 return Ok(UpsertResult {
913 action: UpsertActionKind::Unchanged,
914 row: old_row,
915 auto_inc: None,
916 });
917 }
918 let row = owned_row_from_cells(&merged);
919 self.staging.push((
920 id,
921 Staged::Update {
922 row_id: old_id,
923 new_row: merged,
924 changed_columns,
925 },
926 ));
927 Ok(UpsertResult {
928 action: UpsertActionKind::Updated,
929 row,
930 auto_inc: None,
931 })
932 }
933 }
934 }
935
936 #[doc(hidden)]
939 pub fn upsert_bound(
940 &mut self,
941 table: &str,
942 expected_table_id: u64,
943 expected_schema_id: u64,
944 mut insert_cells: Vec<(u16, Value)>,
945 action: UpsertAction,
946 ) -> Result<UpsertResult> {
947 self.require_columns(table, crate::auth::ColumnOperation::Insert, &insert_cells)?;
948 self.lock_auto_inc_barrier(expected_table_id, &insert_cells)?;
949 let handle = self.bound_table(table, expected_table_id, expected_schema_id)?;
950 self.reject_after_truncate(expected_table_id)?;
951 match (
952 self.existing_pk_row_in(&handle, &insert_cells, expected_table_id)?,
953 action,
954 ) {
955 (None, _) => {
956 let mut target = handle.lock();
957 let assigned = target.fill_auto_inc(&mut insert_cells)?;
958 target.apply_defaults(&mut insert_cells)?;
959 drop(target);
960 let row = owned_row_from_cells(&insert_cells);
961 self.staging
962 .push((expected_table_id, Staged::Put(insert_cells)));
963 Ok(UpsertResult {
964 action: UpsertActionKind::Inserted,
965 row,
966 auto_inc: assigned,
967 })
968 }
969 (Some((_old_id, old_row)), UpsertAction::DoNothing) => Ok(UpsertResult {
970 action: UpsertActionKind::Unchanged,
971 row: old_row,
972 auto_inc: None,
973 }),
974 (Some((old_id, old_row)), UpsertAction::DoUpdate(update_cells)) => {
975 self.require_columns(table, crate::auth::ColumnOperation::Update, &update_cells)?;
976 let changed_columns = changed_columns(&update_cells);
977 let merged = merge_cells(old_row.columns.clone(), update_cells);
978 if columns_equal(&old_row.columns, &merged) {
979 return Ok(UpsertResult {
980 action: UpsertActionKind::Unchanged,
981 row: old_row,
982 auto_inc: None,
983 });
984 }
985 let row = owned_row_from_cells(&merged);
986 self.staging.push((
987 expected_table_id,
988 Staged::Update {
989 row_id: old_id,
990 new_row: merged,
991 changed_columns,
992 },
993 ));
994 Ok(UpsertResult {
995 action: UpsertActionKind::Updated,
996 row,
997 auto_inc: None,
998 })
999 }
1000 }
1001 }
1002
1003 pub fn truncate(&mut self, table: &str) -> Result<()> {
1004 self.db
1005 .require_for(self.principal.as_ref(), &crate::auth::Permission::Admin)?;
1006 let id = self.db.table_id(table)?;
1007 for (table_id, op) in &self.staging {
1008 if *table_id == id && !matches!(op, Staged::Truncate) {
1009 return Err(MongrelError::InvalidArgument(
1010 "truncate cannot be combined with other writes on the same table".into(),
1011 ));
1012 }
1013 }
1014 self.staging.push((id, Staged::Truncate));
1015 Ok(())
1016 }
1017
1018 fn reject_after_truncate(&self, table_id: u64) -> Result<()> {
1019 if self
1020 .staging
1021 .iter()
1022 .any(|(tid, op)| *tid == table_id && matches!(op, Staged::Truncate))
1023 {
1024 return Err(MongrelError::InvalidArgument(
1025 "truncate cannot be combined with other writes on the same table".into(),
1026 ));
1027 }
1028 Ok(())
1029 }
1030
1031 fn require_columns(
1032 &self,
1033 table: &str,
1034 operation: crate::auth::ColumnOperation,
1035 cells: &[(u16, Value)],
1036 ) -> Result<()> {
1037 let columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
1038 self.db
1039 .require_columns_for(table, operation, &columns, self.principal.as_ref())
1040 }
1041
1042 fn require_delete(&self, table: &str) -> Result<()> {
1043 self.db.require_for(
1044 self.principal.as_ref(),
1045 &crate::auth::Permission::Delete {
1046 table: table.to_string(),
1047 },
1048 )
1049 }
1050
1051 fn bound_table(
1052 &self,
1053 table: &str,
1054 expected_table_id: u64,
1055 expected_schema_id: u64,
1056 ) -> Result<TableHandle> {
1057 let current = self.db.table_identity(table)?;
1058 if current != (expected_table_id, expected_schema_id) {
1059 return Err(MongrelError::Conflict(format!(
1060 "table {table:?} changed after request authorization"
1061 )));
1062 }
1063 self.db.table_by_id(expected_table_id)
1064 }
1065
1066 fn existing_pk_row(
1067 &mut self,
1068 table: &str,
1069 cells: &[(u16, Value)],
1070 ) -> Result<Option<(RowId, OwnedRow)>> {
1071 let id = self.db.table_id(table)?;
1072 let handle = self.db.table(table)?;
1073 self.existing_pk_row_in(&handle, cells, id)
1074 }
1075
1076 fn existing_pk_row_in(
1077 &mut self,
1078 handle: &TableHandle,
1079 cells: &[(u16, Value)],
1080 table_id: u64,
1081 ) -> Result<Option<(RowId, OwnedRow)>> {
1082 let snap = self.statement_snapshot();
1083 let target = handle.lock();
1084 let Some(pk_col) = target.schema().primary_key() else {
1085 return Ok(None);
1086 };
1087 let Some((_, pk_value)) = cells.iter().find(|(id, _)| *id == pk_col.id) else {
1088 return Ok(None);
1089 };
1090 if matches!(pk_value, Value::Null) {
1091 return Ok(None);
1092 }
1093 let Some(row_id) = target.lookup_pk(&pk_value.encode_key()) else {
1094 return Ok(None);
1095 };
1096 let found = target
1097 .get(row_id, snap)
1098 .map(|row| (row_id, owned_row_from_map(row.columns)));
1099 if found.is_some() {
1100 self.read_set.record_row(table_id, row_id);
1101 }
1102 Ok(found)
1103 }
1104
1105 pub fn commit(self) -> Result<Epoch> {
1107 self.commit_full(None, None, None).map(|(epoch, _)| epoch)
1108 }
1109
1110 pub fn commit_idempotent(self, request: IdempotencyRequest) -> Result<Epoch> {
1115 self.commit_full(None, None, Some(request))
1116 .map(|(epoch, _)| epoch)
1117 }
1118
1119 pub fn commit_with_row_ids(self) -> Result<(Epoch, Vec<RowId>)> {
1120 self.commit_full(None, None, None)
1121 }
1122
1123 pub fn commit_controlled<F>(
1127 self,
1128 control: &crate::ExecutionControl,
1129 mut before_commit: F,
1130 ) -> Result<Epoch>
1131 where
1132 F: FnMut() -> Result<()>,
1133 {
1134 self.commit_full(Some(control), Some(&mut before_commit), None)
1135 .map(|(epoch, _)| epoch)
1136 }
1137
1138 pub fn commit_controlled_with_row_ids<F>(
1139 self,
1140 control: &crate::ExecutionControl,
1141 mut before_commit: F,
1142 ) -> Result<(Epoch, Vec<RowId>)>
1143 where
1144 F: FnMut() -> Result<()>,
1145 {
1146 self.commit_full(Some(control), Some(&mut before_commit), None)
1147 }
1148
1149 fn commit_full(
1161 mut self,
1162 control: Option<&crate::ExecutionControl>,
1163 before_commit: Option<&mut dyn FnMut() -> Result<()>>,
1164 idempotency: Option<IdempotencyRequest>,
1165 ) -> Result<(Epoch, Vec<RowId>)> {
1166 if let Some(message) = self.allocation_error.take() {
1167 self.state.abort(AbortReason::Error(message.clone()));
1168 return Err(MongrelError::Full(message));
1169 }
1170 if let Err(fault) = mongreldb_fault::inject("txn.prepare.before") {
1173 let error = crate::commit_log::fault_as_io(fault);
1174 classify_commit_error(&self.state, &error);
1175 return Err(error);
1176 }
1177 self.state.begin_prepare();
1178 if let Err(fault) = mongreldb_fault::inject("txn.prepare.after") {
1179 let error = crate::commit_log::fault_as_io(fault);
1180 classify_commit_error(&self.state, &error);
1181 return Err(error);
1182 }
1183 let context = TxnCommitContext {
1184 isolation: self.isolation,
1185 read_ts: self.read_ts,
1186 read_set: std::mem::take(&mut self.read_set),
1187 predicate_set: std::mem::take(&mut self.predicate_set),
1188 state: Some(self.state.clone()),
1189 idempotency,
1190 };
1191 let staging = std::mem::take(&mut self.staging);
1192 let external_states = std::mem::take(&mut self.external_states);
1193 let materialized_view_updates = std::mem::take(&mut self.materialized_view_updates);
1194 let principal = self.principal.take();
1195 let result = match (control, before_commit) {
1196 (Some(control), Some(before_commit)) => {
1197 self.db.commit_transaction_with_external_states_controlled(
1198 self.txn_id,
1199 self.read.epoch,
1200 staging,
1201 external_states,
1202 materialized_view_updates,
1203 principal,
1204 self.principal_catalog_bound,
1205 self.external_trigger_bridge,
1206 context,
1207 control,
1208 before_commit,
1209 )
1210 }
1211 _ => self.db.commit_transaction_with_external_states(
1212 self.txn_id,
1213 self.read.epoch,
1214 staging,
1215 external_states,
1216 materialized_view_updates,
1217 principal,
1218 self.principal_catalog_bound,
1219 self.external_trigger_bridge,
1220 context,
1221 ),
1222 };
1223 if let Err(error) = &result {
1224 classify_commit_error(&self.state, error);
1225 }
1226 result
1227 }
1228
1229 pub fn rollback(self) {
1231 self.state.abort(AbortReason::RolledBack);
1232 }
1234}
1235
1236impl Drop for Transaction<'_> {
1237 fn drop(&mut self) {
1238 self.state.abort(AbortReason::RolledBack);
1242 if self.txn_id != crate::wal::SYSTEM_TXN_ID {
1246 self.db.release_txn_locks(self.txn_id);
1247 }
1248 }
1249}
1250
1251fn owned_row_from_cells(cells: &[(u16, Value)]) -> OwnedRow {
1252 let mut columns = cells.to_vec();
1253 columns.sort_by_key(|(id, _)| *id);
1254 OwnedRow { columns }
1255}
1256
1257fn owned_row_from_map(columns: HashMap<u16, Value>) -> OwnedRow {
1258 let mut columns: Vec<(u16, Value)> = columns.into_iter().collect();
1259 columns.sort_by_key(|(id, _)| *id);
1260 OwnedRow { columns }
1261}
1262
1263fn merge_cells(mut base: Vec<(u16, Value)>, updates: Vec<(u16, Value)>) -> Vec<(u16, Value)> {
1264 for (id, value) in updates {
1265 base.retain(|(existing, _)| *existing != id);
1266 base.push((id, value));
1267 }
1268 base.sort_by_key(|(id, _)| *id);
1269 base
1270}
1271
1272fn changed_columns(cells: &[(u16, Value)]) -> Vec<u16> {
1273 let mut columns = cells.iter().map(|(column, _)| *column).collect::<Vec<_>>();
1274 columns.sort_unstable();
1275 columns.dedup();
1276 columns
1277}
1278
1279fn columns_equal(a: &[(u16, Value)], b: &[(u16, Value)]) -> bool {
1280 if a.len() != b.len() {
1281 return false;
1282 }
1283 let mut a: Vec<_> = a.iter().collect();
1284 let mut b: Vec<_> = b.iter().collect();
1285 a.sort_by_key(|(id, _)| *id);
1286 b.sort_by_key(|(id, _)| *id);
1287 a.iter()
1288 .zip(b.iter())
1289 .all(|((id_a, v_a), (id_b, v_b))| id_a == id_b && v_a == v_b)
1290}
1291
1292pub(crate) enum StagedOp {
1294 Put(Vec<crate::memtable::Row>),
1295 Delete(Vec<RowId>),
1296 Truncate,
1297}
1298
1299use std::collections::{BTreeMap, HashMap};
1302use std::hash::{Hash, Hasher};
1303
1304#[derive(Clone, Debug)]
1307pub enum WriteKey {
1308 Row { table_id: u64, row_id: u64 },
1310 Unique {
1312 table_id: u64,
1313 index_id: u16,
1314 key_hash: u64,
1315 },
1316 Table { table_id: u64 },
1318}
1319
1320impl Hash for WriteKey {
1321 fn hash<H: Hasher>(&self, state: &mut H) {
1322 match self {
1323 WriteKey::Row { table_id, row_id } => {
1324 0u8.hash(state);
1325 table_id.hash(state);
1326 row_id.hash(state);
1327 }
1328 WriteKey::Unique {
1329 table_id,
1330 index_id,
1331 key_hash,
1332 } => {
1333 1u8.hash(state);
1334 table_id.hash(state);
1335 index_id.hash(state);
1336 key_hash.hash(state);
1337 }
1338 WriteKey::Table { table_id } => {
1339 2u8.hash(state);
1340 table_id.hash(state);
1341 }
1342 }
1343 }
1344}
1345
1346impl PartialEq for WriteKey {
1347 fn eq(&self, other: &Self) -> bool {
1348 match (self, other) {
1349 (
1350 WriteKey::Row {
1351 table_id: a,
1352 row_id: b,
1353 },
1354 WriteKey::Row {
1355 table_id: c,
1356 row_id: d,
1357 },
1358 ) => a == c && b == d,
1359 (
1360 WriteKey::Unique {
1361 table_id: a,
1362 index_id: b,
1363 key_hash: c,
1364 },
1365 WriteKey::Unique {
1366 table_id: d,
1367 index_id: e,
1368 key_hash: f,
1369 },
1370 ) => a == d && b == e && c == f,
1371 (WriteKey::Table { table_id: a }, WriteKey::Table { table_id: b }) => a == b,
1372 _ => false,
1373 }
1374 }
1375}
1376
1377impl Eq for WriteKey {}
1378
1379const CONFLICT_SHARDS: usize = 16;
1380
1381pub struct ConflictIndex {
1385 shards: [parking_lot::Mutex<HashMap<WriteKey, u64>>; CONFLICT_SHARDS],
1386 table_truncate_epochs: parking_lot::Mutex<HashMap<u64, u64>>,
1387 table_write_epochs: parking_lot::Mutex<HashMap<u64, u64>>,
1388 version: std::sync::atomic::AtomicU64,
1392 last_prune_floor: std::sync::atomic::AtomicU64,
1396 #[cfg(test)]
1397 prune_passes: std::sync::atomic::AtomicU64,
1398}
1399
1400impl ConflictIndex {
1401 pub fn new() -> Self {
1402 Self {
1403 shards: std::array::from_fn(|_| parking_lot::Mutex::new(HashMap::new())),
1404 table_truncate_epochs: parking_lot::Mutex::new(HashMap::new()),
1405 table_write_epochs: parking_lot::Mutex::new(HashMap::new()),
1406 version: std::sync::atomic::AtomicU64::new(0),
1407 last_prune_floor: std::sync::atomic::AtomicU64::new(0),
1408 #[cfg(test)]
1409 prune_passes: std::sync::atomic::AtomicU64::new(0),
1410 }
1411 }
1412
1413 pub fn version(&self) -> u64 {
1417 self.version.load(std::sync::atomic::Ordering::Acquire)
1418 }
1419
1420 fn shard(&self, key: &WriteKey) -> &parking_lot::Mutex<HashMap<WriteKey, u64>> {
1421 let mut h = std::collections::hash_map::DefaultHasher::new();
1422 key.hash(&mut h);
1423 let idx = (h.finish() as usize) & (CONFLICT_SHARDS - 1);
1424 &self.shards[idx]
1425 }
1426
1427 pub fn conflicts(&self, keys: &[WriteKey], read_epoch: Epoch) -> bool {
1430 for k in keys {
1431 let s = self.shard(k);
1432 if let Some(&ce) = s.lock().get(k) {
1433 if ce > read_epoch.0 {
1434 return true;
1435 }
1436 }
1437 }
1438 let truncates = self.table_truncate_epochs.lock();
1439 let writes = self.table_write_epochs.lock();
1440 for k in keys {
1441 match k {
1442 WriteKey::Row { table_id, .. } | WriteKey::Unique { table_id, .. } => {
1443 if truncates.get(table_id).is_some_and(|&ce| ce > read_epoch.0) {
1444 return true;
1445 }
1446 }
1447 WriteKey::Table { table_id } => {
1448 if writes.get(table_id).is_some_and(|&ce| ce > read_epoch.0) {
1449 return true;
1450 }
1451 }
1452 }
1453 }
1454 false
1455 }
1456
1457 pub fn record(&self, keys: &[WriteKey], commit_epoch: Epoch) {
1459 for k in keys {
1460 let s = self.shard(k);
1461 s.lock().insert(k.clone(), commit_epoch.0);
1462 }
1463 let mut truncates = self.table_truncate_epochs.lock();
1464 let mut writes = self.table_write_epochs.lock();
1465 for k in keys {
1466 match k {
1467 WriteKey::Table { table_id } => {
1468 truncates
1469 .entry(*table_id)
1470 .and_modify(|ce| *ce = (*ce).max(commit_epoch.0))
1471 .or_insert(commit_epoch.0);
1472 }
1473 WriteKey::Row { table_id, .. } | WriteKey::Unique { table_id, .. } => {
1474 writes
1475 .entry(*table_id)
1476 .and_modify(|ce| *ce = (*ce).max(commit_epoch.0))
1477 .or_insert(commit_epoch.0);
1478 }
1479 }
1480 }
1481 self.version
1482 .fetch_add(1, std::sync::atomic::Ordering::Release);
1483 }
1484
1485 pub fn prune_below(&self, min_active: Epoch) {
1488 let previous = self
1489 .last_prune_floor
1490 .fetch_max(min_active.0, std::sync::atomic::Ordering::AcqRel);
1491 if min_active.0 <= previous {
1492 return;
1493 }
1494 #[cfg(test)]
1495 self.prune_passes
1496 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1497
1498 for s in &self.shards {
1499 s.lock().retain(|_, ce| *ce >= min_active.0);
1500 }
1501 self.table_truncate_epochs
1502 .lock()
1503 .retain(|_, ce| *ce >= min_active.0);
1504 self.table_write_epochs
1505 .lock()
1506 .retain(|_, ce| *ce >= min_active.0);
1507 }
1508
1509 #[cfg(test)]
1510 fn prune_passes(&self) -> u64 {
1511 self.prune_passes.load(std::sync::atomic::Ordering::Relaxed)
1512 }
1513}
1514
1515impl Default for ConflictIndex {
1516 fn default() -> Self {
1517 Self::new()
1518 }
1519}
1520
1521pub struct GroupCommit {
1531 inner: PlMutex<GroupState>,
1532 cv: Condvar,
1533 lifecycle: Option<Arc<crate::core::LifecycleController>>,
1537}
1538
1539struct GroupState {
1540 durable_seq: u64,
1541 syncing: bool,
1542 poisoned: bool,
1543}
1544
1545impl GroupCommit {
1546 pub fn new(durable_seq: u64) -> Self {
1547 Self {
1548 inner: PlMutex::new(GroupState {
1549 durable_seq,
1550 syncing: false,
1551 poisoned: false,
1552 }),
1553 cv: Condvar::new(),
1554 lifecycle: None,
1555 }
1556 }
1557
1558 pub fn with_lifecycle(mut self, lifecycle: Arc<crate::core::LifecycleController>) -> Self {
1562 self.lifecycle = Some(lifecycle);
1563 self
1564 }
1565
1566 pub fn await_durable(&self, wal: &PlMutex<SharedWal>, commit_seq: u64) -> Result<()> {
1572 let mut st = self.inner.lock();
1573 loop {
1574 if st.poisoned {
1575 return Err(MongrelError::Other(
1576 "database poisoned by fsync error".into(),
1577 ));
1578 }
1579 if st.durable_seq >= commit_seq {
1580 return Ok(());
1581 }
1582 if st.syncing {
1583 self.cv.wait(&mut st);
1585 continue;
1586 }
1587 st.syncing = true;
1590 drop(st);
1591 std::thread::sleep(std::time::Duration::from_micros(50));
1593 let res = wal.lock().group_sync();
1594 st = self.inner.lock();
1595 st.syncing = false;
1596 match res {
1597 Ok(durable) => {
1598 if durable > st.durable_seq {
1599 st.durable_seq = durable;
1600 }
1601 self.cv.notify_all();
1602 }
1605 Err(e) => {
1606 st.poisoned = true;
1607 if let Some(lifecycle) = &self.lifecycle {
1611 lifecycle.poison();
1612 }
1613 self.cv.notify_all();
1614 return Err(e);
1615 }
1616 }
1617 }
1618 }
1619}
1620
1621pub struct ActiveTxns {
1625 inner: parking_lot::Mutex<BTreeMap<u64, u64>>,
1626}
1627
1628impl ActiveTxns {
1629 pub fn new() -> Self {
1630 Self {
1631 inner: parking_lot::Mutex::new(BTreeMap::new()),
1632 }
1633 }
1634
1635 pub fn register(&self, read_epoch: Epoch) -> ActiveTxnGuard<'_> {
1638 let mut g = self.inner.lock();
1639 *g.entry(read_epoch.0).or_insert(0) += 1;
1640 ActiveTxnGuard {
1641 active: self,
1642 epoch: read_epoch.0,
1643 }
1644 }
1645
1646 pub fn min_read_epoch(&self) -> u64 {
1648 self.inner.lock().keys().next().copied().unwrap_or(u64::MAX)
1649 }
1650}
1651
1652impl Default for ActiveTxns {
1653 fn default() -> Self {
1654 Self::new()
1655 }
1656}
1657
1658pub struct ActiveTxnGuard<'a> {
1660 active: &'a ActiveTxns,
1661 epoch: u64,
1662}
1663
1664impl Drop for ActiveTxnGuard<'_> {
1665 fn drop(&mut self) {
1666 let mut g = self.active.inner.lock();
1667 if let Some(count) = g.get_mut(&self.epoch) {
1668 *count -= 1;
1669 if *count == 0 {
1670 g.remove(&self.epoch);
1671 }
1672 }
1673 }
1674}
1675
1676#[cfg(test)]
1677mod tests {
1678 use super::*;
1679
1680 #[test]
1681 fn shared_transaction_allocator_never_crosses_open_generation() {
1682 let allocator = PlMutex::new((7_u64 << 32) | u32::MAX as u64);
1683 assert_eq!(
1684 allocate_txn_id(&allocator).unwrap(),
1685 (7_u64 << 32) | u32::MAX as u64
1686 );
1687 assert!(matches!(
1688 allocate_txn_id(&allocator),
1689 Err(MongrelError::Full(_))
1690 ));
1691 }
1692
1693 #[test]
1694 fn conflict_index_first_committer_wins_and_prunes_safely() {
1695 let ci = ConflictIndex::new();
1696 let k = vec![WriteKey::Row {
1697 table_id: 1,
1698 row_id: 7,
1699 }];
1700 assert!(!ci.conflicts(&k, Epoch(5)));
1701 ci.record(&k, Epoch(6));
1702 assert!(ci.conflicts(&k, Epoch(5)));
1703 assert!(!ci.conflicts(&k, Epoch(6)));
1704 ci.prune_below(Epoch(7));
1705 assert!(!ci.conflicts(&k, Epoch(5)));
1706 }
1707
1708 #[test]
1709 fn conflict_index_skips_repeated_unchanged_prune_floor() {
1710 let ci = ConflictIndex::new();
1711 let first = WriteKey::Row {
1712 table_id: 1,
1713 row_id: 7,
1714 };
1715 let second = WriteKey::Row {
1716 table_id: 1,
1717 row_id: 8,
1718 };
1719
1720 ci.record(std::slice::from_ref(&first), Epoch(6));
1721 ci.prune_below(Epoch(5));
1722 assert_eq!(ci.prune_passes(), 1);
1723
1724 ci.record(std::slice::from_ref(&second), Epoch(7));
1727 ci.prune_below(Epoch(5));
1728 assert_eq!(ci.prune_passes(), 1);
1729 assert!(ci.conflicts(std::slice::from_ref(&first), Epoch(4)));
1730 assert!(ci.conflicts(std::slice::from_ref(&second), Epoch(4)));
1731
1732 ci.prune_below(Epoch(8));
1733 assert_eq!(ci.prune_passes(), 2);
1734 assert!(!ci.conflicts(&[first, second], Epoch(4)));
1735 }
1736
1737 #[test]
1738 fn conflict_index_table_scope_conflicts_both_directions() {
1739 let ci = ConflictIndex::new();
1740 ci.record(&[WriteKey::Table { table_id: 1 }], Epoch(6));
1741 assert!(ci.conflicts(
1742 &[WriteKey::Row {
1743 table_id: 1,
1744 row_id: 7,
1745 }],
1746 Epoch(5)
1747 ));
1748 assert!(ci.conflicts(
1749 &[WriteKey::Unique {
1750 table_id: 1,
1751 index_id: 0,
1752 key_hash: 42,
1753 }],
1754 Epoch(5)
1755 ));
1756 assert!(!ci.conflicts(
1757 &[WriteKey::Row {
1758 table_id: 2,
1759 row_id: 7,
1760 }],
1761 Epoch(5)
1762 ));
1763
1764 let ci = ConflictIndex::new();
1765 ci.record(
1766 &[WriteKey::Row {
1767 table_id: 1,
1768 row_id: 7,
1769 }],
1770 Epoch(6),
1771 );
1772 assert!(ci.conflicts(&[WriteKey::Table { table_id: 1 }], Epoch(5)));
1773 assert!(!ci.conflicts(&[WriteKey::Table { table_id: 2 }], Epoch(5)));
1774 }
1775
1776 #[test]
1777 fn writekey_eq_across_variants() {
1778 let r1 = WriteKey::Row {
1779 table_id: 1,
1780 row_id: 2,
1781 };
1782 let r2 = WriteKey::Row {
1783 table_id: 1,
1784 row_id: 2,
1785 };
1786 let r3 = WriteKey::Row {
1787 table_id: 1,
1788 row_id: 3,
1789 };
1790 assert_eq!(r1, r2);
1791 assert_ne!(r1, r3);
1792
1793 let u1 = WriteKey::Unique {
1794 table_id: 1,
1795 index_id: 0,
1796 key_hash: 42,
1797 };
1798 let u2 = WriteKey::Unique {
1799 table_id: 1,
1800 index_id: 0,
1801 key_hash: 42,
1802 };
1803 assert_eq!(u1, u2);
1804 assert_ne!(r1, u1);
1805
1806 let t1 = WriteKey::Table { table_id: 5 };
1807 let t2 = WriteKey::Table { table_id: 5 };
1808 assert_eq!(t1, t2);
1809 assert_ne!(t1, r1);
1810 }
1811
1812 #[test]
1813 fn active_txns_tracks_min_read_epoch() {
1814 let at = ActiveTxns::new();
1815 assert_eq!(at.min_read_epoch(), u64::MAX);
1816 let g1 = at.register(Epoch(5));
1817 assert_eq!(at.min_read_epoch(), 5);
1818 let g2 = at.register(Epoch(3));
1819 assert_eq!(at.min_read_epoch(), 3);
1820 drop(g2);
1821 assert_eq!(at.min_read_epoch(), 5);
1822 drop(g1);
1823 assert_eq!(at.min_read_epoch(), u64::MAX);
1824 }
1825
1826 #[test]
1827 fn active_txns_dedups_same_epoch() {
1828 let at = ActiveTxns::new();
1829 let g1 = at.register(Epoch(7));
1830 let g2 = at.register(Epoch(7));
1831 assert_eq!(at.min_read_epoch(), 7);
1832 drop(g1);
1833 assert_eq!(at.min_read_epoch(), 7);
1834 drop(g2);
1835 assert_eq!(at.min_read_epoch(), u64::MAX);
1836 }
1837
1838 #[test]
1839 fn isolation_level_snapshot_aliases_repeatable_read() {
1840 #[allow(deprecated)]
1841 let snapshot = IsolationLevel::Snapshot;
1842 assert_eq!(snapshot.canonical(), IsolationLevel::RepeatableRead);
1843 assert_eq!(IsolationLevel::default(), IsolationLevel::RepeatableRead);
1844 assert_eq!(
1845 IsolationLevel::ReadCommitted.canonical(),
1846 IsolationLevel::ReadCommitted
1847 );
1848 assert_eq!(
1849 IsolationLevel::Serializable.canonical(),
1850 IsolationLevel::Serializable
1851 );
1852 }
1853
1854 #[test]
1855 fn transaction_state_transitions_are_enforced() {
1856 let handle = TxnStateHandle::new();
1857 assert!(matches!(handle.state(), TransactionState::Active));
1858 assert!(!handle.enter_commit_critical());
1860 assert!(matches!(handle.state(), TransactionState::Active));
1861 let receipt = mongreldb_log::CommitReceipt {
1863 transaction_id: mongreldb_types::ids::TransactionId::from_bytes([0; 16]),
1864 commit_ts: HlcTimestamp::ZERO,
1865 log_position: mongreldb_log::LogPosition::ZERO,
1866 durability: mongreldb_log::DurabilityLevel::GroupCommit,
1867 };
1868 assert!(!handle.committed(receipt.clone()));
1869
1870 assert!(handle.begin_prepare());
1871 assert!(matches!(handle.state(), TransactionState::Preparing));
1872 assert!(handle.enter_commit_critical());
1873 assert!(matches!(handle.state(), TransactionState::CommitCritical));
1874 assert!(!handle.abort(AbortReason::Conflict("late".into())));
1876 assert!(matches!(handle.state(), TransactionState::CommitCritical));
1877 assert!(handle.committed(receipt));
1878 assert!(matches!(handle.state(), TransactionState::Committed(_)));
1879 assert!(!handle.abort(AbortReason::RolledBack));
1881 assert!(!handle.begin_prepare());
1882 }
1883
1884 #[test]
1885 fn abort_from_preparing_is_terminal() {
1886 let handle = TxnStateHandle::new();
1887 assert!(handle.abort(AbortReason::RolledBack));
1888 assert!(matches!(
1889 handle.state(),
1890 TransactionState::Aborted(AbortReason::RolledBack)
1891 ));
1892 assert!(!handle.begin_prepare());
1893
1894 let handle = TxnStateHandle::new();
1895 handle.begin_prepare();
1896 assert!(handle.abort(AbortReason::Validation("bad row".into())));
1897 match handle.state() {
1898 TransactionState::Aborted(AbortReason::Validation(message)) => {
1899 assert_eq!(message, "bad row")
1900 }
1901 other => panic!("expected validation abort, got {other:?}"),
1902 }
1903 }
1904
1905 #[test]
1906 fn classify_commit_error_leaves_post_fence_states_untouched() {
1907 let handle = TxnStateHandle::new();
1908 handle.begin_prepare();
1909 classify_commit_error(&handle, &MongrelError::Conflict("ww".into()));
1910 assert!(matches!(
1911 handle.state(),
1912 TransactionState::Aborted(AbortReason::Conflict(_))
1913 ));
1914
1915 let handle = TxnStateHandle::new();
1916 handle.begin_prepare();
1917 handle.enter_commit_critical();
1918 classify_commit_error(
1919 &handle,
1920 &MongrelError::CommitOutcomeUnknown {
1921 epoch: 7,
1922 message: "fsync".into(),
1923 },
1924 );
1925 assert!(matches!(handle.state(), TransactionState::CommitCritical));
1926 classify_commit_error(
1927 &handle,
1928 &MongrelError::DurableCommit {
1929 epoch: 7,
1930 message: "publish".into(),
1931 },
1932 );
1933 assert!(matches!(handle.state(), TransactionState::CommitCritical));
1934 }
1935
1936 #[test]
1937 fn classify_commit_error_maps_serialization_failure_to_conflict_abort() {
1938 let handle = TxnStateHandle::new();
1942 handle.begin_prepare();
1943 classify_commit_error(
1944 &handle,
1945 &MongrelError::SerializationFailure {
1946 message: "a concurrent commit invalidated this transaction's reads".into(),
1947 },
1948 );
1949 match handle.state() {
1950 TransactionState::Aborted(AbortReason::Conflict(message)) => {
1951 assert_eq!(
1952 message,
1953 "serialization failure: a concurrent commit invalidated this transaction's reads"
1954 );
1955 }
1956 other => panic!("expected conflict abort, got {other:?}"),
1957 }
1958 }
1959
1960 #[test]
1961 fn ssi_validation_keys_cover_rows_and_predicates() {
1962 let mut reads = ReadSet::default();
1963 reads.record_row(3, RowId(9));
1964 reads.record_row(3, RowId(9));
1965 reads.record_row(4, RowId(1));
1966 let mut predicates = PredicateSet::default();
1967 predicates.record_table(5);
1968 let keys = ssi_validation_keys(&reads, &predicates);
1969 assert_eq!(keys.len(), 3);
1970 assert!(keys.iter().any(|key| matches!(
1971 key,
1972 WriteKey::Row {
1973 table_id: 3,
1974 row_id: 9
1975 }
1976 )));
1977 assert!(keys.iter().any(|key| matches!(
1978 key,
1979 WriteKey::Row {
1980 table_id: 4,
1981 row_id: 1
1982 }
1983 )));
1984 assert!(keys
1985 .iter()
1986 .any(|key| matches!(key, WriteKey::Table { table_id: 5 })));
1987 }
1988
1989 fn test_request(key: &str) -> IdempotencyRequest {
1990 IdempotencyRequest {
1991 key: key.to_string(),
1992 owner: "alice".to_string(),
1993 fingerprint: 42,
1994 ttl: None,
1995 }
1996 }
1997
1998 fn test_commit_ts() -> HlcTimestamp {
1999 HlcTimestamp {
2000 physical_micros: 1_700_000_000_000_000,
2001 logical: 3,
2002 node_tiebreaker: 0,
2003 }
2004 }
2005
2006 #[test]
2007 fn idempotency_ledger_replay_conflict_and_restart() {
2008 let dir = tempfile::tempdir().unwrap();
2009 let root = std::sync::Arc::new(crate::durable_file::DurableRoot::open(dir.path()).unwrap());
2010 let ledger = IdempotencyLedger::open(std::sync::Arc::clone(&root), None).unwrap();
2011
2012 let request = test_request("k1");
2013 assert!(matches!(
2014 ledger.check_and_reserve(&request).unwrap(),
2015 IdempotencyCheck::Reserved
2016 ));
2017 assert!(matches!(
2019 ledger.check_and_reserve(&request),
2020 Err(MongrelError::Conflict(_))
2021 ));
2022 ledger
2023 .complete(&request, 7, Epoch(11), test_commit_ts())
2024 .unwrap();
2025
2026 let replay = ledger.check_and_reserve(&request).unwrap();
2028 let IdempotencyCheck::Replay(receipt) = replay else {
2029 panic!("expected replay");
2030 };
2031 assert_eq!(receipt.log_position.index, 11);
2032 assert_eq!(receipt.commit_ts, test_commit_ts());
2033 assert_eq!(
2034 receipt.durability,
2035 mongreldb_log::DurabilityLevel::GroupCommit
2036 );
2037
2038 let mut other = test_request("k1");
2040 other.fingerprint = 43;
2041 assert!(matches!(
2042 ledger.check_and_reserve(&other),
2043 Err(MongrelError::Conflict(_))
2044 ));
2045 let mut foreign = test_request("k1");
2047 foreign.owner = "bob".to_string();
2048 assert!(matches!(
2049 ledger.check_and_reserve(&foreign).unwrap(),
2050 IdempotencyCheck::Reserved
2051 ));
2052 drop(ledger);
2053
2054 let reopened = IdempotencyLedger::open(root, None).unwrap();
2056 let replay = reopened.check_and_reserve(&request).unwrap();
2057 let IdempotencyCheck::Replay(receipt) = replay else {
2058 panic!("expected replay after restart");
2059 };
2060 assert_eq!(receipt.log_position.index, 11);
2061 assert_eq!(receipt.commit_ts, test_commit_ts());
2062 assert!(matches!(
2064 reopened.check_and_reserve(&foreign),
2065 Err(MongrelError::Conflict(_))
2066 ));
2067 }
2068
2069 #[test]
2070 fn idempotency_ledger_release_and_expiry() {
2071 let dir = tempfile::tempdir().unwrap();
2072 let root = std::sync::Arc::new(crate::durable_file::DurableRoot::open(dir.path()).unwrap());
2073 let ledger = IdempotencyLedger::open(std::sync::Arc::clone(&root), None).unwrap();
2074
2075 let request = test_request("k-release");
2077 assert!(matches!(
2078 ledger.check_and_reserve(&request).unwrap(),
2079 IdempotencyCheck::Reserved
2080 ));
2081 ledger.release(&request);
2082 assert!(matches!(
2083 ledger.check_and_reserve(&request).unwrap(),
2084 IdempotencyCheck::Reserved
2085 ));
2086 ledger
2087 .complete(&request, 9, Epoch(12), test_commit_ts())
2088 .unwrap();
2089
2090 let mut expired = test_request("k-expired");
2093 expired.ttl = Some(Duration::from_nanos(1));
2094 assert!(matches!(
2095 ledger.check_and_reserve(&expired).unwrap(),
2096 IdempotencyCheck::Reserved
2097 ));
2098 ledger
2099 .complete(&expired, 10, Epoch(13), test_commit_ts())
2100 .unwrap();
2101 std::thread::sleep(Duration::from_millis(2));
2102 assert!(matches!(
2103 ledger.check_and_reserve(&expired).unwrap(),
2104 IdempotencyCheck::Reserved
2105 ));
2106
2107 drop(ledger);
2109 let reopened = IdempotencyLedger::open(root, None).unwrap();
2110 assert!(matches!(
2111 reopened.check_and_reserve(&expired).unwrap(),
2112 IdempotencyCheck::Reserved
2113 ));
2114 }
2115
2116 #[test]
2117 fn idempotency_ledger_rejects_empty_key_or_owner() {
2118 let dir = tempfile::tempdir().unwrap();
2119 let root = std::sync::Arc::new(crate::durable_file::DurableRoot::open(dir.path()).unwrap());
2120 let ledger = IdempotencyLedger::open(root, None).unwrap();
2121 let mut request = test_request("");
2122 assert!(matches!(
2123 ledger.check_and_reserve(&request),
2124 Err(MongrelError::InvalidArgument(_))
2125 ));
2126 request.key = "k".to_string();
2127 request.owner.clear();
2128 assert!(matches!(
2129 ledger.check_and_reserve(&request),
2130 Err(MongrelError::InvalidArgument(_))
2131 ));
2132 }
2133
2134 #[test]
2135 fn idempotency_ledger_enforces_bounded_size() {
2136 let mut records: Vec<StoredIdempotencyRecord> = (0..MAX_IDEMPOTENCY_RECORDS + 4)
2137 .map(|index| StoredIdempotencyRecord {
2138 owner: "o".to_string(),
2139 key: format!("k{index}"),
2140 fingerprint: 1,
2141 expires_at_micros: None,
2142 outcome: StoredIdempotencyOutcome::Committed {
2143 txn_id: index as u64,
2144 epoch: index as u64,
2145 commit_ts: test_commit_ts(),
2146 },
2147 })
2148 .collect();
2149 enforce_bounds(&mut records).unwrap();
2150 assert_eq!(records.len(), MAX_IDEMPOTENCY_RECORDS);
2151 assert!(records.iter().all(|record| match &record.outcome {
2153 StoredIdempotencyOutcome::Committed { epoch, .. } => *epoch >= 4,
2154 StoredIdempotencyOutcome::Reserved => false,
2155 }));
2156
2157 let mut reserved: Vec<StoredIdempotencyRecord> = (0..MAX_IDEMPOTENCY_RECORDS + 1)
2160 .map(|index| StoredIdempotencyRecord {
2161 owner: "o".to_string(),
2162 key: format!("r{index}"),
2163 fingerprint: 1,
2164 expires_at_micros: None,
2165 outcome: StoredIdempotencyOutcome::Reserved,
2166 })
2167 .collect();
2168 assert!(matches!(
2169 enforce_bounds(&mut reserved),
2170 Err(MongrelError::ResourceLimitExceeded { .. })
2171 ));
2172 }
2173
2174 #[test]
2175 fn idempotency_ledger_tampered_file_fails_closed() {
2176 let dir = tempfile::tempdir().unwrap();
2177 let root = std::sync::Arc::new(crate::durable_file::DurableRoot::open(dir.path()).unwrap());
2178 let ledger = IdempotencyLedger::open(std::sync::Arc::clone(&root), None).unwrap();
2179 let request = test_request("k1");
2180 ledger.check_and_reserve(&request).unwrap();
2181 drop(ledger);
2182
2183 let path = dir.path().join(IDEMPOTENCY_FILENAME);
2184 let mut bytes = std::fs::read(&path).unwrap();
2185 let last = bytes.len() - 1;
2186 bytes[last] ^= 0xFF;
2187 std::fs::write(&path, bytes).unwrap();
2188 assert!(IdempotencyLedger::open(root, None).is_err());
2189 }
2190}
2191
2192#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2212pub enum IsolationLevel {
2213 #[default]
2214 RepeatableRead,
2215 #[deprecated(
2216 note = "renamed to `RepeatableRead` (spec §10.2 S1B-002); identical semantics, kept for compatibility"
2217 )]
2218 Snapshot,
2219 ReadCommitted,
2220 Serializable,
2221}
2222
2223impl IsolationLevel {
2224 pub fn canonical(self) -> Self {
2227 match self {
2228 #[allow(deprecated)]
2229 Self::Snapshot => Self::RepeatableRead,
2230 other => other,
2231 }
2232 }
2233}
2234
2235use std::time::Duration;
2238
2239#[derive(Debug, Clone)]
2242pub struct IdempotencyRequest {
2243 pub key: String,
2245 pub owner: String,
2247 pub fingerprint: u64,
2251 pub ttl: Option<Duration>,
2254}
2255
2256pub const IDEMPOTENCY_FILENAME: &str = "TXN_IDEMPOTENCY";
2260
2261const IDEMPOTENCY_FORMAT_VERSION: u16 = 1;
2262const IDEMPOTENCY_MAGIC: &[u8; 8] = b"MONGRTXI";
2263const MAX_IDEMPOTENCY_RECORDS: usize = 65_536;
2266const MAX_IDEMPOTENCY_BYTES: u64 = 64 * 1024 * 1024;
2268
2269#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2271enum StoredIdempotencyOutcome {
2272 Reserved,
2275 Committed {
2277 txn_id: u64,
2278 epoch: u64,
2279 commit_ts: HlcTimestamp,
2280 },
2281}
2282
2283#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2284struct StoredIdempotencyRecord {
2285 owner: String,
2286 key: String,
2287 fingerprint: u64,
2288 expires_at_micros: Option<u64>,
2289 outcome: StoredIdempotencyOutcome,
2290}
2291
2292#[derive(serde::Serialize, serde::Deserialize)]
2293struct IdempotencyEnvelope {
2294 format_version: u16,
2295 records: Vec<StoredIdempotencyRecord>,
2296}
2297
2298pub(crate) enum IdempotencyCheck {
2300 Reserved,
2303 Replay(mongreldb_log::CommitReceipt),
2306}
2307
2308pub(crate) struct IdempotencyLedger {
2314 root: std::sync::Arc<crate::durable_file::DurableRoot>,
2315 meta_dek: Option<[u8; crate::catalog::META_DEK_LEN]>,
2316 inner: PlMutex<Vec<StoredIdempotencyRecord>>,
2317}
2318
2319impl IdempotencyLedger {
2320 pub(crate) fn open(
2325 root: std::sync::Arc<crate::durable_file::DurableRoot>,
2326 meta_dek: Option<[u8; crate::catalog::META_DEK_LEN]>,
2327 ) -> Result<Self> {
2328 let mut inner = read_idempotency_file(&root, meta_dek.as_ref())?.unwrap_or_default();
2329 sweep_expired(&mut inner, wall_micros());
2330 Ok(Self {
2331 root,
2332 meta_dek,
2333 inner: PlMutex::new(inner),
2334 })
2335 }
2336
2337 pub(crate) fn check_and_reserve(
2342 &self,
2343 request: &IdempotencyRequest,
2344 ) -> Result<IdempotencyCheck> {
2345 validate_request(request)?;
2346 let now = wall_micros();
2347 let mut inner = self.inner.lock();
2348 sweep_expired(&mut inner, now);
2349 if let Some(existing) = inner
2350 .iter()
2351 .find(|record| record.owner == request.owner && record.key == request.key)
2352 {
2353 if existing.fingerprint != request.fingerprint {
2354 return Err(MongrelError::Conflict(format!(
2355 "idempotency key {:?} was already used with a different request",
2356 request.key
2357 )));
2358 }
2359 return match &existing.outcome {
2360 StoredIdempotencyOutcome::Committed {
2361 txn_id,
2362 epoch,
2363 commit_ts,
2364 } => Ok(IdempotencyCheck::Replay(mongreldb_log::CommitReceipt {
2365 transaction_id: crate::commit_log::transaction_id_from_txn(*txn_id),
2366 commit_ts: *commit_ts,
2367 log_position: mongreldb_log::LogPosition {
2368 term: 0,
2369 index: *epoch,
2370 },
2371 durability: mongreldb_log::DurabilityLevel::GroupCommit,
2372 })),
2373 StoredIdempotencyOutcome::Reserved => Err(MongrelError::Conflict(format!(
2374 "idempotency key {:?} has an in-flight or interrupted commit; retry to resolve",
2375 request.key
2376 ))),
2377 };
2378 }
2379 inner.push(StoredIdempotencyRecord {
2380 owner: request.owner.clone(),
2381 key: request.key.clone(),
2382 fingerprint: request.fingerprint,
2383 expires_at_micros: expiry_micros(request.ttl, now),
2384 outcome: StoredIdempotencyOutcome::Reserved,
2385 });
2386 persist_locked(&self.root, self.meta_dek.as_ref(), &mut inner)?;
2387 Ok(IdempotencyCheck::Reserved)
2388 }
2389
2390 pub(crate) fn complete(
2396 &self,
2397 request: &IdempotencyRequest,
2398 txn_id: u64,
2399 epoch: Epoch,
2400 commit_ts: HlcTimestamp,
2401 ) -> Result<()> {
2402 let mut inner = self.inner.lock();
2403 let now = wall_micros();
2404 let Some(record) = inner
2405 .iter_mut()
2406 .find(|record| record.owner == request.owner && record.key == request.key)
2407 else {
2408 return Err(MongrelError::Other(format!(
2409 "idempotency reservation for key {:?} vanished during commit",
2410 request.key
2411 )));
2412 };
2413 record.expires_at_micros = expiry_micros(request.ttl, now);
2414 record.outcome = StoredIdempotencyOutcome::Committed {
2415 txn_id,
2416 epoch: epoch.0,
2417 commit_ts,
2418 };
2419 persist_locked(&self.root, self.meta_dek.as_ref(), &mut inner)
2420 }
2421
2422 pub(crate) fn release(&self, request: &IdempotencyRequest) {
2426 let mut inner = self.inner.lock();
2427 inner.retain(|record| {
2428 !(record.owner == request.owner
2429 && record.key == request.key
2430 && matches!(record.outcome, StoredIdempotencyOutcome::Reserved))
2431 });
2432 let _ = persist_locked(&self.root, self.meta_dek.as_ref(), &mut inner);
2433 }
2434}
2435
2436pub(crate) struct IdempotencyReservationGuard<'a> {
2439 ledger: &'a IdempotencyLedger,
2440 request: IdempotencyRequest,
2441 armed: bool,
2442}
2443
2444impl<'a> IdempotencyReservationGuard<'a> {
2445 pub(crate) fn new(ledger: &'a IdempotencyLedger, request: IdempotencyRequest) -> Self {
2446 Self {
2447 ledger,
2448 request,
2449 armed: true,
2450 }
2451 }
2452
2453 pub(crate) fn disarm(&mut self) {
2454 self.armed = false;
2455 }
2456}
2457
2458impl Drop for IdempotencyReservationGuard<'_> {
2459 fn drop(&mut self) {
2460 if self.armed {
2461 self.ledger.release(&self.request);
2462 }
2463 }
2464}
2465
2466fn validate_request(request: &IdempotencyRequest) -> Result<()> {
2467 if request.key.is_empty() {
2468 return Err(MongrelError::InvalidArgument(
2469 "idempotency key must not be empty".into(),
2470 ));
2471 }
2472 if request.owner.is_empty() {
2473 return Err(MongrelError::InvalidArgument(
2474 "idempotency owner must not be empty".into(),
2475 ));
2476 }
2477 Ok(())
2478}
2479
2480fn expiry_micros(ttl: Option<Duration>, now_micros: u64) -> Option<u64> {
2481 ttl.map(|ttl| now_micros.saturating_add(u64::try_from(ttl.as_micros()).unwrap_or(u64::MAX)))
2482}
2483
2484fn sweep_expired(records: &mut Vec<StoredIdempotencyRecord>, now_micros: u64) {
2485 records.retain(|record| {
2486 record
2487 .expires_at_micros
2488 .is_none_or(|expires_at| expires_at > now_micros)
2489 });
2490}
2491
2492fn enforce_bounds(records: &mut Vec<StoredIdempotencyRecord>) -> Result<()> {
2496 while records.len() > MAX_IDEMPOTENCY_RECORDS {
2497 let Some((oldest, _)) = records
2498 .iter()
2499 .enumerate()
2500 .filter(|(_, record)| {
2501 matches!(record.outcome, StoredIdempotencyOutcome::Committed { .. })
2502 })
2503 .min_by_key(|(_, record)| match &record.outcome {
2504 StoredIdempotencyOutcome::Committed { epoch, .. } => *epoch,
2505 StoredIdempotencyOutcome::Reserved => unreachable!(),
2506 })
2507 else {
2508 return Err(MongrelError::ResourceLimitExceeded {
2509 resource: "idempotency records",
2510 requested: records.len(),
2511 limit: MAX_IDEMPOTENCY_RECORDS,
2512 });
2513 };
2514 records.remove(oldest);
2515 }
2516 Ok(())
2517}
2518
2519fn persist_locked(
2520 root: &crate::durable_file::DurableRoot,
2521 meta_dek: Option<&[u8; crate::catalog::META_DEK_LEN]>,
2522 records: &mut Vec<StoredIdempotencyRecord>,
2523) -> Result<()> {
2524 enforce_bounds(records)?;
2525 let body = serde_json::to_vec(&IdempotencyEnvelope {
2526 format_version: IDEMPOTENCY_FORMAT_VERSION,
2527 records: records.clone(),
2528 })
2529 .map_err(|error| MongrelError::Other(format!("idempotency ledger serialize: {error}")))?;
2530 let payload = seal_idempotency(&body, meta_dek)?;
2531 root.write_atomic(IDEMPOTENCY_FILENAME, &payload)?;
2532 Ok(())
2533}
2534
2535fn read_idempotency_file(
2536 root: &crate::durable_file::DurableRoot,
2537 meta_dek: Option<&[u8; crate::catalog::META_DEK_LEN]>,
2538) -> Result<Option<Vec<StoredIdempotencyRecord>>> {
2539 use std::io::Read as _;
2540 let file = match root.open_regular(IDEMPOTENCY_FILENAME) {
2541 Ok(file) => file,
2542 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
2543 Err(error) => return Err(error.into()),
2544 };
2545 let length = file.metadata()?.len();
2546 if length > MAX_IDEMPOTENCY_BYTES {
2547 return Err(MongrelError::Other(format!(
2548 "idempotency ledger of {length} bytes exceeds the {MAX_IDEMPOTENCY_BYTES}-byte limit"
2549 )));
2550 }
2551 let mut bytes = Vec::with_capacity(length as usize);
2552 file.take(MAX_IDEMPOTENCY_BYTES + 1)
2553 .read_to_end(&mut bytes)?;
2554 if bytes.len() as u64 != length {
2555 return Err(MongrelError::Other(
2556 "idempotency ledger length changed while reading".into(),
2557 ));
2558 }
2559 open_idempotency_payload(&bytes, meta_dek).map(Some)
2560}
2561
2562fn decode_idempotency(body: &[u8]) -> Result<Vec<StoredIdempotencyRecord>> {
2563 let envelope: IdempotencyEnvelope = serde_json::from_slice(body)
2564 .map_err(|error| MongrelError::Other(format!("idempotency ledger deserialize: {error}")))?;
2565 if envelope.format_version != IDEMPOTENCY_FORMAT_VERSION {
2566 return Err(MongrelError::Other(format!(
2567 "unsupported idempotency ledger format version {}",
2568 envelope.format_version
2569 )));
2570 }
2571 Ok(envelope.records)
2572}
2573
2574fn plaintext_idempotency_frame(body: &[u8]) -> Vec<u8> {
2575 use sha2::Digest as _;
2576 let hash = sha2::Sha256::digest(body);
2577 let mut out = Vec::with_capacity(body.len() + 8 + 32);
2578 out.extend_from_slice(IDEMPOTENCY_MAGIC);
2579 out.extend_from_slice(&hash);
2580 out.extend_from_slice(body);
2581 out
2582}
2583
2584fn parse_idempotency_plaintext(bytes: &[u8]) -> Result<Vec<StoredIdempotencyRecord>> {
2585 use sha2::Digest as _;
2586 if bytes.len() < 8 + 32 || &bytes[..8] != IDEMPOTENCY_MAGIC {
2587 return Err(MongrelError::Other(
2588 "idempotency ledger magic mismatch (corrupt or sealed with a key)".into(),
2589 ));
2590 }
2591 let (tag, body) = bytes[8..].split_at(32);
2592 let calc = sha2::Sha256::digest(body);
2593 if tag != calc.as_slice() {
2594 return Err(MongrelError::Other(
2595 "idempotency ledger checksum mismatch (tampered or torn)".into(),
2596 ));
2597 }
2598 decode_idempotency(body)
2599}
2600
2601fn seal_idempotency(
2602 body: &[u8],
2603 meta_dek: Option<&[u8; crate::catalog::META_DEK_LEN]>,
2604) -> Result<Vec<u8>> {
2605 match meta_dek {
2606 Some(dek) => crate::encryption::encrypt_blob(dek, body),
2607 None => Ok(plaintext_idempotency_frame(body)),
2608 }
2609}
2610
2611fn open_idempotency_payload(
2612 bytes: &[u8],
2613 meta_dek: Option<&[u8; crate::catalog::META_DEK_LEN]>,
2614) -> Result<Vec<StoredIdempotencyRecord>> {
2615 match meta_dek {
2616 Some(dek) => {
2619 let body = crate::encryption::decrypt_blob(dek, bytes).map_err(|_| {
2620 MongrelError::Decryption(
2621 "idempotency ledger authentication failed (wrong key or tampered)".into(),
2622 )
2623 })?;
2624 decode_idempotency(&body)
2625 }
2626 None => parse_idempotency_plaintext(bytes),
2627 }
2628}
2629
2630fn wall_micros() -> u64 {
2632 let micros = std::time::SystemTime::now()
2633 .duration_since(std::time::UNIX_EPOCH)
2634 .map(|duration| duration.as_micros())
2635 .unwrap_or(0);
2636 u64::try_from(micros).unwrap_or(u64::MAX)
2637}