Skip to main content

nexir_mvcc/
engine.rs

1use crate::backend::Backend;
2use crate::error::{
3    AbortError, BatchAbortError, BatchCommitError, BatchError, BatchPrewriteError, CommitError,
4    GcError, PrewriteError, ReadError,
5};
6use crate::types::{
7    CommittedVersion, Intent, Mutation, PhysicalWrite, ReadGuard, Timestamp, TxnId,
8};
9
10/// Statistics produced by garbage collection.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct GcStats {
13    /// The number of obsolete versions physically removed.
14    pub versions_removed: usize,
15    /// The number of intents safely ignored and preserved.
16    pub intents_preserved: usize,
17}
18
19/// Budget for incremental garbage collection.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct GcBudget {
22    /// Maximum number of keys to process in one step.
23    pub max_keys: usize,
24    /// Maximum number of historical versions to remove in one step.
25    pub max_versions: usize,
26}
27
28/// Options for garbage collection operations.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct GcOptions {
31    /// Budget parameters limiting the scope of one step.
32    pub budget: GcBudget,
33    /// Explicit opt-in retention policy: if true, a final unshadowed tombstone
34    /// will be physically collapsed. This is ONLY safe if the caller guarantees
35    /// a strict low-watermark where no future reads, prewrites, or guards will
36    /// ever be issued at or below the collapsed tombstone timestamp.
37    pub collapse_final_tombstones: bool,
38}
39
40/// Options for a read-only, single-key garbage-collection plan.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct KeyGcOptions {
43    /// Maximum number of obsolete, pre-keeper version timestamps to examine.
44    ///
45    /// The planner also performs a fixed number of bounded point lookups to
46    /// locate the safe-point keeper and determine whether it is the latest
47    /// committed version. Those point lookups do not consume this history-page
48    /// budget.
49    pub max_versions_examined: usize,
50    /// Explicit opt-in retention policy for final tombstones.
51    ///
52    /// Callers using this opt-in feature MUST ensure that the safe point is
53    /// derived from a monotonic log index and that no future read or write will
54    /// occur below this safe point, as collapsing final tombstones changes
55    /// `read_with_version` and guard history semantics below the safe point.
56    pub collapse_final_tombstones: bool,
57}
58
59/// A deterministic, read-only garbage-collection plan for one logical key.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct KeyGcPlan {
62    /// The exact logical key that was planned.
63    pub key: Vec<u8>,
64    /// Explicit obsolete version timestamps to remove, ordered newest first.
65    ///
66    /// This list never includes the safe-point keeper. If
67    /// [`Self::collapse_tombstone`] is present, the caller must remove that
68    /// tombstone and these older versions atomically.
69    pub versions_to_remove: Vec<Timestamp>,
70    /// The final tombstone timestamp to collapse, when explicitly permitted.
71    pub collapse_tombstone: Option<Timestamp>,
72    /// Number of obsolete, pre-keeper version timestamps examined.
73    pub versions_examined: usize,
74    /// Whether the planner established that no eligible debt remains unplanned.
75    ///
76    /// A page that exactly fills the budget is conservatively incomplete,
77    /// because the bounded backend contract does not require a separate
78    /// unbudgeted lookahead read. After applying the explicit removals, planning
79    /// the same key again will establish completion or return another page.
80    pub complete: bool,
81}
82
83/// Cursor to resume incremental garbage collection.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct IncrementalGcCursor {
86    /// The next key to evaluate.
87    pub next_key: Option<Vec<u8>>,
88}
89
90/// Result of an incremental garbage collection step.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct IncrementalGcResult {
93    /// The cursor to resume GC from.
94    pub cursor: IncrementalGcCursor,
95    /// True if a full pass over the keyspace has completed.
96    pub done: bool,
97    /// Number of keys scanned in this step.
98    pub keys_scanned: usize,
99    /// Number of versions scanned in this step.
100    pub versions_scanned: usize,
101    /// Number of versions physically removed in this step.
102    pub versions_removed: usize,
103    /// Number of active intents safely ignored and preserved.
104    pub intents_preserved: usize,
105}
106
107/// The central state machine for executing MVCC operations.
108///
109/// `MvccEngine` wraps a durable `Backend` and provides high-level APIs for
110/// single-key and multi-key reads, transactional intents, and direct batches.
111pub struct MvccEngine<B: Backend> {
112    backend: B,
113}
114
115impl<B: Backend> MvccEngine<B> {
116    /// Creates a new `MvccEngine` with the given backend.
117    pub fn new(backend: B) -> Self {
118        Self { backend }
119    }
120
121    /// Returns an immutable reference to the underlying backend.
122    pub fn backend(&self) -> &B {
123        &self.backend
124    }
125
126    /// Returns a mutable reference to the underlying backend.
127    pub fn backend_mut(&mut self) -> &mut B {
128        &mut self.backend
129    }
130
131    /// Reads the visible value for a key at a given logical `read_ts`.
132    ///
133    /// This method ignores uncommitted intents and finds the newest committed
134    /// version that has a `commit_ts` <= `read_ts`.
135    pub fn read(&self, key: &[u8], read_ts: Timestamp) -> Result<Option<Vec<u8>>, ReadError> {
136        let version = self
137            .backend
138            .get_visible_committed(key, read_ts)
139            .map_err(ReadError::Backend)?;
140        Ok(version.and_then(|v| v.value))
141    }
142
143    /// Reads the visible value for a key at `read_ts` and returns its commit timestamp and value.
144    ///
145    /// If no version is visible at or before `read_ts`, returns `Ok(None)`.
146    /// If a tombstone is visible, returns `Ok(Some((ts, None)))`.
147    /// If a value is visible, returns `Ok(Some((ts, Some(value))))`.
148    #[allow(clippy::type_complexity)]
149    pub fn read_with_version(
150        &self,
151        key: &[u8],
152        read_ts: Timestamp,
153    ) -> Result<Option<(Timestamp, Option<Vec<u8>>)>, ReadError> {
154        let version = self
155            .backend
156            .get_visible_committed(key, read_ts)
157            .map_err(ReadError::Backend)?;
158        Ok(version.map(|v| (v.commit_ts, v.value)))
159    }
160
161    /// Reads the visible value for a key, prioritizing the transaction's own active intent.
162    ///
163    /// If the transaction has an active intent on the key, its value is returned.
164    /// Otherwise, it falls back to a normal historical read at `read_ts`.
165    pub fn read_own_write(
166        &self,
167        key: &[u8],
168        txn_id: TxnId,
169        start_ts: Timestamp,
170        read_ts: Timestamp,
171    ) -> Result<Option<Vec<u8>>, ReadError> {
172        // First, check if this txn has an intent on the key.
173        if let Some(intent) = self.backend.get_intent(key).map_err(ReadError::Backend)?
174            && intent.txn_id == txn_id
175            && intent.start_ts == start_ts
176        {
177            return Ok(intent.mutation.value());
178        }
179        // Fall back to committed versions.
180        self.read(key, read_ts)
181    }
182
183    /// Prewrites a single intent for a distributed transaction.
184    ///
185    /// Fails if another transaction holds an intent on the key, or if a newer
186    /// committed version exists (write conflict). It is idempotent for the same transaction
187    /// and `start_ts`.
188    pub fn prewrite(
189        &mut self,
190        txn_id: TxnId,
191        start_ts: Timestamp,
192        key: Vec<u8>,
193        mutation: Mutation,
194    ) -> Result<(), PrewriteError> {
195        // Check for an existing intent by another txn.
196        if let Some(intent) = self
197            .backend
198            .get_intent(&key)
199            .map_err(PrewriteError::Backend)?
200        {
201            if intent.txn_id != txn_id {
202                return Err(PrewriteError::KeyLocked {
203                    txn_id: intent.txn_id,
204                });
205            }
206            // Same txn: if start_ts matches, this is a re-prewrite (idempotent).
207            // If start_ts differs, it's a conflicting intent from the same txn
208            // (should not happen in correct usage).
209            if intent.start_ts != start_ts {
210                return Err(PrewriteError::IntentAlreadyExists);
211            }
212            // Same txn, same start_ts: already prewritten. Verify mutation matches.
213            if intent.mutation != mutation {
214                return Err(PrewriteError::IntentAlreadyExists);
215            }
216            return Ok(());
217        }
218
219        // Check for write conflict: any committed version with commit_ts > start_ts.
220        if let Some(latest_ts) = self
221            .backend
222            .get_latest_commit_ts(&key)
223            .map_err(PrewriteError::Backend)?
224            && latest_ts > start_ts
225        {
226            return Err(PrewriteError::WriteConflict);
227        }
228
229        let intent = Intent {
230            key: key.clone(),
231            txn_id,
232            start_ts,
233            mutation,
234            min_commit_ts: None,
235        };
236        self.backend
237            .put_intent(intent)
238            .map_err(PrewriteError::Backend)?;
239        Ok(())
240    }
241
242    /// Commits a single intent, creating a durable version.
243    ///
244    /// Converts the intent at `start_ts` into a committed version at `commit_ts`.
245    /// The backend must execute this atomically (create version and remove intent).
246    pub fn commit(
247        &mut self,
248        txn_id: TxnId,
249        key: &[u8],
250        start_ts: Timestamp,
251        commit_ts: Timestamp,
252    ) -> Result<(), CommitError> {
253        let intent = self
254            .backend
255            .get_intent(key)
256            .map_err(CommitError::Backend)?
257            .ok_or(CommitError::IntentNotFound)?;
258
259        if intent.txn_id != txn_id {
260            return Err(CommitError::TxnIdMismatch);
261        }
262        if intent.start_ts != start_ts {
263            return Err(CommitError::StartTsMismatch);
264        }
265
266        if commit_ts <= start_ts {
267            return Err(CommitError::InvalidCommitTimestamp {
268                start_ts,
269                commit_ts,
270            });
271        }
272
273        if let Some(min_ts) = intent.min_commit_ts
274            && commit_ts < min_ts
275        {
276            return Err(CommitError::CommitTsTooEarly {
277                commit_ts,
278                min_commit_ts: min_ts,
279            });
280        }
281
282        // Check for retroactive commit
283        if let Some(latest_ts) = self
284            .backend
285            .get_latest_commit_ts(key)
286            .map_err(CommitError::Backend)?
287            && commit_ts <= latest_ts
288        {
289            if commit_ts == latest_ts {
290                return Err(CommitError::DuplicateCommitTimestamp { commit_ts });
291            } else {
292                return Err(CommitError::CommitTsTooOld {
293                    commit_ts,
294                    latest_commit_ts: latest_ts,
295                });
296            }
297        }
298
299        // Create committed version and remove the intent in one backend transition.
300        let version = CommittedVersion {
301            key: key.to_vec(),
302            commit_ts,
303            value: intent.mutation.value(),
304        };
305        self.backend
306            .commit_intents_batch(vec![version], vec![(key.to_vec(), txn_id, start_ts)])
307            .map_err(CommitError::Backend)?;
308        Ok(())
309    }
310
311    /// Aborts a single intent, removing it from the backend.
312    ///
313    /// This method is idempotent: if the intent is not found, it returns `Ok(())`.
314    pub fn abort(
315        &mut self,
316        txn_id: TxnId,
317        key: &[u8],
318        start_ts: Timestamp,
319    ) -> Result<(), AbortError> {
320        let removed = self
321            .backend
322            .remove_intent(key, txn_id, start_ts)
323            .map_err(AbortError::Backend)?;
324        // If the intent was not found or did not match, it's a no-op.
325        // This makes abort idempotent.
326        let _ = removed;
327        Ok(())
328    }
329
330    /// Prewrites multiple intents atomically for a transaction.
331    ///
332    /// Rejects empty batches with `EmptyBatch`. Identical replay semantics apply
333    /// as in single-key prewrite.
334    pub fn prewrite_batch(
335        &mut self,
336        txn_id: TxnId,
337        start_ts: Timestamp,
338        writes: Vec<PhysicalWrite>,
339    ) -> Result<(), BatchPrewriteError> {
340        if writes.is_empty() {
341            return Err(BatchPrewriteError::EmptyBatch);
342        }
343
344        let mut key_set = std::collections::HashSet::new();
345        for w in &writes {
346            if !key_set.insert(w.key.clone()) {
347                return Err(BatchPrewriteError::DuplicateKeyInBatch { key: w.key.clone() });
348            }
349        }
350
351        let mut existing_count = 0;
352        for w in &writes {
353            if let Some(intent) = self
354                .backend
355                .get_intent(&w.key)
356                .map_err(BatchPrewriteError::Backend)?
357            {
358                if intent.txn_id != txn_id {
359                    return Err(BatchPrewriteError::KeyLocked {
360                        key: w.key.clone(),
361                        txn_id: intent.txn_id,
362                    });
363                }
364                let expected_mutation = if let Some(v) = &w.value {
365                    Mutation::Put(v.clone())
366                } else {
367                    Mutation::Delete
368                };
369                if intent.start_ts != start_ts || intent.mutation != expected_mutation {
370                    return Err(BatchPrewriteError::IntentAlreadyExists { key: w.key.clone() });
371                }
372                existing_count += 1;
373            } else if let Some(latest_ts) = self
374                .backend
375                .get_latest_commit_ts(&w.key)
376                .map_err(BatchPrewriteError::Backend)?
377                && latest_ts > start_ts
378            {
379                return Err(BatchPrewriteError::WriteConflict { key: w.key.clone() });
380            }
381        }
382
383        if existing_count == writes.len() {
384            return Ok(());
385        } else if existing_count > 0 {
386            return Err(BatchPrewriteError::PartialBatchReplay);
387        }
388
389        let mut intents = Vec::with_capacity(writes.len());
390        for w in writes {
391            intents.push(Intent {
392                key: w.key,
393                txn_id,
394                start_ts,
395                mutation: if let Some(v) = w.value {
396                    Mutation::Put(v)
397                } else {
398                    Mutation::Delete
399                },
400                min_commit_ts: None,
401            });
402        }
403        self.backend
404            .put_intents_batch(intents)
405            .map_err(BatchPrewriteError::Backend)?;
406        Ok(())
407    }
408
409    /// Commits multiple intents atomically, creating durable versions.
410    ///
411    /// Rejects empty batches with `EmptyBatch`.
412    pub fn commit_batch(
413        &mut self,
414        txn_id: TxnId,
415        start_ts: Timestamp,
416        commit_ts: Timestamp,
417        keys: Vec<Vec<u8>>,
418    ) -> Result<(), BatchCommitError> {
419        if keys.is_empty() {
420            return Err(BatchCommitError::EmptyBatch);
421        }
422
423        let mut key_set = std::collections::HashSet::new();
424        for key in &keys {
425            if !key_set.insert(key.clone()) {
426                return Err(BatchCommitError::DuplicateKeyInBatch { key: key.clone() });
427            }
428        }
429
430        if commit_ts <= start_ts {
431            return Err(BatchCommitError::InvalidCommitTimestamp {
432                start_ts,
433                commit_ts,
434            });
435        }
436
437        let mut commits = Vec::with_capacity(keys.len());
438        let mut removed_intents = Vec::with_capacity(keys.len());
439
440        for key in &keys {
441            let intent = self
442                .backend
443                .get_intent(key)
444                .map_err(BatchCommitError::Backend)?
445                .ok_or_else(|| BatchCommitError::IntentNotFound { key: key.clone() })?;
446
447            if intent.txn_id != txn_id {
448                return Err(BatchCommitError::TxnIdMismatch { key: key.clone() });
449            }
450            if intent.start_ts != start_ts {
451                return Err(BatchCommitError::StartTsMismatch { key: key.clone() });
452            }
453            if let Some(min_ts) = intent.min_commit_ts
454                && commit_ts < min_ts
455            {
456                return Err(BatchCommitError::CommitTsTooEarly {
457                    key: key.clone(),
458                    commit_ts,
459                    min_commit_ts: min_ts,
460                });
461            }
462
463            if let Some(latest_ts) = self
464                .backend
465                .get_latest_commit_ts(key)
466                .map_err(BatchCommitError::Backend)?
467                && commit_ts <= latest_ts
468            {
469                return Err(BatchCommitError::CommitTsTooOld {
470                    key: key.clone(),
471                    commit_ts,
472                    latest_commit_ts: latest_ts,
473                });
474            }
475
476            commits.push(CommittedVersion {
477                key: key.clone(),
478                commit_ts,
479                value: intent.mutation.value(),
480            });
481            removed_intents.push((key.clone(), txn_id, start_ts));
482        }
483
484        self.backend
485            .commit_intents_batch(commits, removed_intents)
486            .map_err(BatchCommitError::Backend)?;
487        Ok(())
488    }
489
490    /// Aborts multiple intents, removing them atomically.
491    ///
492    /// If the key list is empty, it returns `Ok(())` (idempotent).
493    pub fn abort_batch(
494        &mut self,
495        txn_id: TxnId,
496        start_ts: Timestamp,
497        keys: Vec<Vec<u8>>,
498    ) -> Result<(), BatchAbortError> {
499        if keys.is_empty() {
500            return Ok(());
501        }
502
503        let mut key_set = std::collections::HashSet::new();
504        for key in &keys {
505            if !key_set.insert(key.clone()) {
506                return Err(BatchAbortError::DuplicateKeyInBatch { key: key.clone() });
507            }
508        }
509
510        let mut removed_intents = Vec::with_capacity(keys.len());
511        for key in &keys {
512            if let Some(intent) = self
513                .backend
514                .get_intent(key)
515                .map_err(BatchAbortError::Backend)?
516                && intent.txn_id == txn_id
517                && intent.start_ts == start_ts
518            {
519                removed_intents.push((key.clone(), txn_id, start_ts));
520            }
521        }
522
523        self.backend
524            .remove_intents_batch(removed_intents)
525            .map_err(BatchAbortError::Backend)?;
526        Ok(())
527    }
528
529    /// Applies a batch of writes directly, bypassing intents.
530    ///
531    /// Rejects empty batches with `EmptyBatch`. Fails if any key has an active intent
532    /// or if `commit_ts` is not strictly greater than the latest committed version.
533    pub fn apply_direct_batch(
534        &mut self,
535        commit_ts: Timestamp,
536        writes: Vec<PhysicalWrite>,
537    ) -> Result<(), BatchError> {
538        if writes.is_empty() {
539            return Err(BatchError::EmptyBatch);
540        }
541
542        let mut key_set = std::collections::HashSet::new();
543        for w in &writes {
544            if !key_set.insert(w.key.clone()) {
545                return Err(BatchError::DuplicateKeyInBatch { key: w.key.clone() });
546            }
547        }
548
549        // Validate all writes
550        for w in &writes {
551            // Reject active intents
552            if let Some(intent) = self
553                .backend
554                .get_intent(&w.key)
555                .map_err(BatchError::Backend)?
556            {
557                return Err(BatchError::KeyLocked {
558                    key: w.key.clone(),
559                    txn_id: intent.txn_id,
560                });
561            }
562
563            // Reject commit_ts <= latest_commit_ts
564            if let Some(latest_ts) = self
565                .backend
566                .get_latest_commit_ts(&w.key)
567                .map_err(BatchError::Backend)?
568                && commit_ts <= latest_ts
569            {
570                return Err(BatchError::CommitTsTooOld {
571                    key: w.key.clone(),
572                    commit_ts,
573                    latest_commit_ts: latest_ts,
574                });
575            }
576        }
577
578        // Apply all-or-nothing
579        let mut commits = Vec::with_capacity(writes.len());
580        for w in writes {
581            commits.push(CommittedVersion {
582                key: w.key,
583                commit_ts,
584                value: w.value,
585            });
586        }
587
588        self.backend
589            .put_committed_batch(commits)
590            .map_err(BatchError::Backend)?;
591        Ok(())
592    }
593
594    /// Applies a batch of writes conditionally, validating read guards first.
595    ///
596    /// Useful for Read-Modify-Write operations (e.g. Compare-And-Swap) without
597    /// interactive two-phase commit transactions. Rejects empty writes or empty guards.
598    pub fn apply_guarded_batch(
599        &mut self,
600        commit_ts: Timestamp,
601        guards: Vec<ReadGuard>,
602        writes: Vec<PhysicalWrite>,
603    ) -> Result<(), BatchError> {
604        if writes.is_empty() {
605            return Err(BatchError::EmptyBatch);
606        }
607        if guards.is_empty() {
608            return Err(BatchError::NoReadGuards);
609        }
610
611        let mut write_keys = std::collections::HashSet::new();
612        for w in &writes {
613            if !write_keys.insert(w.key.clone()) {
614                return Err(BatchError::DuplicateKeyInBatch { key: w.key.clone() });
615            }
616        }
617
618        // Validate all guards
619        for guard in &guards {
620            let (guard_key, guard_read_ts) = match guard {
621                ReadGuard::ExpectedVersion { key, read_ts, .. } => (key, read_ts),
622                ReadGuard::ExpectedValue { key, read_ts, .. } => (key, read_ts),
623            };
624
625            if commit_ts <= *guard_read_ts {
626                return Err(BatchError::InvalidCommitTimestamp {
627                    read_ts: *guard_read_ts,
628                    commit_ts,
629                });
630            }
631
632            // Check active intents
633            if let Some(intent) = self
634                .backend
635                .get_intent(guard_key)
636                .map_err(BatchError::Backend)?
637            {
638                return Err(BatchError::KeyLocked {
639                    key: guard_key.clone(),
640                    txn_id: intent.txn_id,
641                });
642            }
643
644            if let Some(latest_ts) = self
645                .backend
646                .get_latest_commit_ts(guard_key)
647                .map_err(BatchError::Backend)?
648                && latest_ts > *guard_read_ts
649            {
650                return Err(BatchError::GuardFailedNewerVersion {
651                    key: guard_key.clone(),
652                    read_ts: *guard_read_ts,
653                    actual_commit_ts: latest_ts,
654                });
655            }
656
657            let visible_version = self
658                .backend
659                .get_visible_committed(guard_key, *guard_read_ts)
660                .map_err(BatchError::Backend)?;
661
662            match guard {
663                ReadGuard::ExpectedVersion {
664                    expected_commit_ts, ..
665                } => {
666                    let actual_commit_ts = visible_version.as_ref().map(|v| v.commit_ts);
667                    if actual_commit_ts != *expected_commit_ts {
668                        return Err(BatchError::GuardFailedVersionMismatch {
669                            key: guard_key.clone(),
670                            expected: *expected_commit_ts,
671                            actual: actual_commit_ts,
672                        });
673                    }
674                }
675                ReadGuard::ExpectedValue { expected_value, .. } => {
676                    let actual_value = visible_version.as_ref().and_then(|v| v.value.as_ref());
677                    if actual_value != expected_value.as_ref() {
678                        return Err(BatchError::GuardFailedValueMismatch {
679                            key: guard_key.clone(),
680                        });
681                    }
682                }
683            }
684        }
685
686        // Validate write keys against intents and duplicate versions
687        for w in &writes {
688            if let Some(intent) = self
689                .backend
690                .get_intent(&w.key)
691                .map_err(BatchError::Backend)?
692            {
693                return Err(BatchError::KeyLocked {
694                    key: w.key.clone(),
695                    txn_id: intent.txn_id,
696                });
697            }
698
699            if let Some(latest_ts) = self
700                .backend
701                .get_latest_commit_ts(&w.key)
702                .map_err(BatchError::Backend)?
703                && commit_ts <= latest_ts
704            {
705                return Err(BatchError::CommitTsTooOld {
706                    key: w.key.clone(),
707                    commit_ts,
708                    latest_commit_ts: latest_ts,
709                });
710            }
711        }
712
713        // Apply all-or-nothing
714        let mut commits = Vec::with_capacity(writes.len());
715        for w in writes {
716            commits.push(CommittedVersion {
717                key: w.key,
718                commit_ts,
719                value: w.value,
720            });
721        }
722
723        self.backend
724            .put_committed_batch(commits)
725            .map_err(BatchError::Backend)?;
726        Ok(())
727    }
728
729    /// Produces a bounded, deterministic GC plan for one logical key.
730    ///
731    /// Planning never mutates the backend. The newest committed version visible
732    /// at `safe_point_ts` is the keeper, and ordinary removals are restricted to
733    /// explicit timestamps strictly older than that keeper. Versions newer than
734    /// the safe point are never included.
735    ///
736    /// A final tombstone can be returned in [`KeyGcPlan::collapse_tombstone`]
737    /// only when the caller opted in, the key has no active intent, the
738    /// tombstone is the latest committed version, and the complete older history
739    /// fits below the planning budget. The caller must remove the tombstone and
740    /// every timestamp in [`KeyGcPlan::versions_to_remove`] atomically.
741    pub fn plan_key_gc(
742        &self,
743        key: &[u8],
744        safe_point_ts: Timestamp,
745        options: KeyGcOptions,
746    ) -> Result<KeyGcPlan, GcError> {
747        if options.max_versions_examined == 0 {
748            return Err(GcError::InvalidKeyGcBudget);
749        }
750
751        let Some(keeper) = self
752            .backend
753            .get_visible_committed(key, safe_point_ts)
754            .map_err(GcError::Backend)?
755        else {
756            return Ok(KeyGcPlan {
757                key: key.to_vec(),
758                versions_to_remove: Vec::new(),
759                collapse_tombstone: None,
760                versions_examined: 0,
761                complete: true,
762            });
763        };
764
765        let collapse_final_tombstone = if options.collapse_final_tombstones
766            && keeper.value.is_none()
767            && self
768                .backend
769                .get_intent(key)
770                .map_err(GcError::Backend)?
771                .is_none()
772        {
773            self.backend
774                .get_latest_commit_ts(key)
775                .map_err(GcError::Backend)?
776                == Some(keeper.commit_ts)
777        } else {
778            false
779        };
780
781        let mut versions_to_remove = self
782            .backend
783            .get_committed_timestamps_before(key, keeper.commit_ts, options.max_versions_examined)
784            .map_err(GcError::Backend)?;
785
786        let complete = versions_to_remove.len() < options.max_versions_examined;
787        versions_to_remove.truncate(options.max_versions_examined);
788        let versions_examined = versions_to_remove.len();
789
790        Ok(KeyGcPlan {
791            key: key.to_vec(),
792            versions_to_remove,
793            collapse_tombstone: collapse_final_tombstone
794                .then_some(keeper.commit_ts)
795                .filter(|_| complete),
796            versions_examined,
797            complete,
798        })
799    }
800
801    /// Performs an incremental step of garbage collection.
802    ///
803    /// Obsolete versions older than `safe_point_ts` are removed up to the `budget`.
804    pub fn gc_incremental(
805        &mut self,
806        safe_point_ts: Timestamp,
807        cursor: Option<IncrementalGcCursor>,
808        options: GcOptions,
809    ) -> Result<IncrementalGcResult, GcError> {
810        if options.budget.max_keys == 0 || options.budget.max_versions == 0 {
811            return Err(GcError::InvalidGcBudget);
812        }
813
814        let start_key = cursor.and_then(|c| c.next_key);
815
816        let keys = self
817            .backend
818            .keys_from(start_key.as_deref(), options.budget.max_keys + 1)
819            .map_err(GcError::Backend)?;
820
821        let mut keys_scanned = 0;
822        let mut versions_scanned = 0;
823        let mut versions_removed = 0;
824        let mut intents_preserved = 0;
825
826        let mut next_cursor_key = None;
827        let mut done = false;
828        let mut exhausted_versions = false;
829
830        let num_keys_to_process = std::cmp::min(keys.len(), options.budget.max_keys);
831
832        for key in keys.iter().take(num_keys_to_process) {
833            keys_scanned += 1;
834
835            let has_intent = self
836                .backend
837                .get_intent(key)
838                .map_err(GcError::Backend)?
839                .is_some();
840            if has_intent {
841                intents_preserved += 1;
842            }
843
844            let keeper = self
845                .backend
846                .get_visible_committed(key, safe_point_ts)
847                .map_err(GcError::Backend)?;
848
849            if let Some(keeper_ver) = keeper {
850                versions_scanned += 1; // Count the keeper lookup
851
852                let limit = options.budget.max_versions - versions_removed;
853                if limit == 0 {
854                    // We check if there are actually any versions to remove before breaking
855                    let check_more = self
856                        .backend
857                        .get_committed_timestamps_before(key, keeper_ver.commit_ts, 1)
858                        .map_err(GcError::Backend)?;
859
860                    if !check_more.is_empty() {
861                        next_cursor_key = Some(key.clone());
862                        exhausted_versions = true;
863                        break;
864                    }
865
866                    // Out of budget, but maybe it's a final tombstone?
867                    // We need at least 1 budget unit to remove it, so we must revisit next time.
868                    if options.collapse_final_tombstones
869                        && keeper_ver.value.is_none()
870                        && !has_intent
871                        && let Some(latest_ts) = self
872                            .backend
873                            .get_latest_commit_ts(key)
874                            .map_err(GcError::Backend)?
875                        && latest_ts == keeper_ver.commit_ts
876                    {
877                        next_cursor_key = Some(key.clone());
878                        exhausted_versions = true;
879                        break;
880                    }
881                    continue;
882                }
883
884                // Check if this is a final tombstone collapse case
885                let mut is_final_tombstone = false;
886                if options.collapse_final_tombstones
887                    && keeper_ver.value.is_none()
888                    && !has_intent
889                    && let Some(latest_ts) = self
890                        .backend
891                        .get_latest_commit_ts(key)
892                        .map_err(GcError::Backend)?
893                    && latest_ts == keeper_ver.commit_ts
894                {
895                    is_final_tombstone = true;
896                }
897
898                if is_final_tombstone {
899                    // Final tombstone case: we must remove the tombstone and all older versions atomically.
900                    // We query up to limit + 1 older versions to see if they all fit in the remaining budget.
901                    let mut older_versions = self
902                        .backend
903                        .get_committed_timestamps_before(key, keeper_ver.commit_ts, limit + 1)
904                        .map_err(GcError::Backend)?;
905
906                    versions_scanned += older_versions.len();
907
908                    let has_more = older_versions.len() > limit;
909                    if has_more {
910                        // There are more older versions than the remaining budget.
911                        // Pop the extra to only remove `limit` older versions.
912                        older_versions.pop();
913                        versions_scanned -= 1;
914
915                        // We cannot delete the tombstone yet, because the remaining older versions
916                        // exceed the budget. We only delete the older versions.
917                        for ts in older_versions {
918                            self.backend
919                                .remove_committed_version(key, ts)
920                                .map_err(GcError::Backend)?;
921                            versions_removed += 1;
922                        }
923
924                        next_cursor_key = Some(key.clone());
925                        exhausted_versions = true;
926                        break;
927                    } else {
928                        // older_versions.len() <= limit.
929                        // The total versions to remove (older versions + tombstone) is older_versions.len() + 1.
930                        let older_len = older_versions.len();
931                        if older_len < limit {
932                            // Fits in the remaining budget! Collapse them atomically.
933                            self.backend
934                                .collapse_tombstone(key, keeper_ver.commit_ts, older_versions)
935                                .map_err(GcError::Backend)?;
936
937                            versions_removed += older_len + 1;
938                        } else {
939                            // older_versions.len() == limit.
940                            // The total versions to remove (older_versions.len() + 1) exceeds the remaining budget (limit).
941                            // We can only remove the older versions in this pass, leaving the tombstone.
942                            for ts in older_versions {
943                                self.backend
944                                    .remove_committed_version(key, ts)
945                                    .map_err(GcError::Backend)?;
946                                versions_removed += 1;
947                            }
948
949                            next_cursor_key = Some(key.clone());
950                            exhausted_versions = true;
951                            break;
952                        }
953                    }
954                } else {
955                    // Normal GC case (not a final tombstone): we only remove older versions.
956                    let mut to_remove = self
957                        .backend
958                        .get_committed_timestamps_before(key, keeper_ver.commit_ts, limit + 1)
959                        .map_err(GcError::Backend)?;
960
961                    versions_scanned += to_remove.len();
962
963                    let has_more = to_remove.len() > limit;
964                    if has_more {
965                        to_remove.pop();
966                        versions_scanned -= 1;
967                    }
968
969                    for ts in to_remove {
970                        self.backend
971                            .remove_committed_version(key, ts)
972                            .map_err(GcError::Backend)?;
973                        versions_removed += 1;
974                    }
975
976                    if has_more {
977                        next_cursor_key = Some(key.clone());
978                        exhausted_versions = true;
979                        break;
980                    }
981                }
982            }
983        }
984
985        if !exhausted_versions {
986            if keys.len() > options.budget.max_keys {
987                next_cursor_key = Some(keys[options.budget.max_keys].clone());
988            } else {
989                done = true;
990            }
991        }
992
993        Ok(IncrementalGcResult {
994            cursor: IncrementalGcCursor {
995                next_key: next_cursor_key,
996            },
997            done,
998            keys_scanned,
999            versions_scanned,
1000            versions_removed,
1001            intents_preserved,
1002        })
1003    }
1004
1005    /// Performs a full garbage collection by repeatedly calling `gc_incremental`.
1006    ///
1007    /// This is maintained for compatibility.
1008    //
1009    // When removing the following method, you have to transform the following tests
1010    //
1011    // Test                        Lines
1012    // ----                        -----
1013    // tests/integration_tests.rs  97, 140, 514, 552, 606, 729, 1041, 1083
1014    // tests/gc_tests.rs           343, 378
1015    // tests/support/model.rs      50, 93
1016    // tests/property_tests.rs     168
1017    // benches/core_bench.rs       457
1018    #[deprecated(
1019        note = "unbounded: loops gc_incremental to completion and materializes the whole \
1020                keyspace via all_keys(); production must use budgeted gc_incremental"
1021    )]
1022    pub fn gc(&mut self, safe_point_ts: Timestamp, options: GcOptions) -> Result<GcStats, GcError> {
1023        let mut total_versions_removed = 0;
1024
1025        let mut cursor = None;
1026
1027        loop {
1028            let res = self.gc_incremental(safe_point_ts, cursor.take(), options)?;
1029            total_versions_removed += res.versions_removed;
1030
1031            if res.done {
1032                break;
1033            }
1034            cursor = Some(res.cursor);
1035        }
1036
1037        let mut total_intents_preserved = 0;
1038        for key in self.backend.all_keys().map_err(GcError::Backend)? {
1039            if self
1040                .backend
1041                .get_intent(&key)
1042                .map_err(GcError::Backend)?
1043                .is_some()
1044            {
1045                total_intents_preserved += 1;
1046            }
1047        }
1048
1049        Ok(GcStats {
1050            versions_removed: total_versions_removed,
1051            intents_preserved: total_intents_preserved,
1052        })
1053    }
1054}