Skip to main content

uqa_storage/key_value/diskann/
reclamation.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Bounded deletion uses committed retirement authority; retained readers keep their MVCC history.
8
9use crate::diskann_index::format::DiskANNGeneration;
10use crate::key_value::KeyValueRead;
11use crate::read_control::StorageReadControl;
12use crate::{KeyValueBatch, StorageBackendResult};
13
14use super::keys::{database_key, Keys, Kind};
15use super::source::{key_page, KEY_PAGE_LIMIT};
16use super::staging::load_state;
17use super::state::{StageOwner, State};
18use super::{invalid, DiskANNStageStatus, KeyValueDiskANNStore};
19
20#[derive(Clone, Copy, PartialEq, Eq)]
21pub(super) enum Reclamation {
22    Complete,
23    More,
24    Retained,
25}
26
27#[derive(Clone, Copy, PartialEq, Eq)]
28pub(super) enum ReclamationMode {
29    Explicit,
30    Maintenance,
31}
32
33impl KeyValueDiskANNStore {
34    /// Reclaim an unpublished generation only after exclusively acquiring its abandoned physical owner. A live build, retained sealed source or uncertain original attempt returns `false` without deleting anything. Each successful step deletes at most 64 payload records; partial cleanup persists Discarding and can resume through either reclamation method. Current published heads are always rejected.
35    pub fn reclaim_abandoned_step(
36        &self,
37        generation: DiskANNGeneration,
38        max_records: usize,
39        control: &StorageReadControl,
40    ) -> StorageBackendResult<bool> {
41        self.reclaim_generation(generation, max_records, ReclamationMode::Explicit, control)
42            .map(|result| result == Reclamation::Complete)
43    }
44
45    pub(super) fn reclaim_generation(
46        &self,
47        generation: DiskANNGeneration,
48        max_records: usize,
49        mode: ReclamationMode,
50        control: &StorageReadControl,
51    ) -> StorageBackendResult<Reclamation> {
52        control.check()?;
53        if max_records == 0 {
54            return Err(invalid("reclamation requires a positive record limit"));
55        }
56        let mut observed = None;
57        {
58            let _writer = self.owner.writer.lock();
59            self.idle(control)?;
60            self.owner.store.with_read_view(&mut |read| {
61                observed = load_state(read, generation, control)?;
62                Ok(())
63            })?;
64        }
65        let Some(state) = observed else {
66            return self
67                .reclaim_retired_step(generation, max_records, control)
68                .map(Reclamation::from);
69        };
70        match state.status {
71            DiskANNStageStatus::Published => {
72                if mode == ReclamationMode::Maintenance {
73                    return Ok(Reclamation::Retained);
74                }
75                return Err(invalid("published generation cannot be abandoned"));
76            }
77            DiskANNStageStatus::Retired | DiskANNStageStatus::Discarding => {
78                return self
79                    .reclaim_retired_step(generation, max_records, control)
80                    .map(Reclamation::from);
81            }
82            DiskANNStageStatus::Writing
83            | DiskANNStageStatus::Frozen
84            | DiskANNStageStatus::Sealed => {}
85        }
86        let lease = match state.owner {
87            StageOwner::Legacy(_) => {
88                let Some(legacy) = self.legacy_guard(control)? else {
89                    return Ok(Reclamation::Retained);
90                };
91                let lease = self.reserve_owner(control)?;
92                Self::retain_transition(&lease, legacy, control)?
93            }
94            StageOwner::Leased(_) => {
95                let Some(lease) = self.acquire_owner(
96                    state.owner,
97                    crate::mvcc::ResourceLeaseRequest::Recover,
98                    control,
99                )?
100                else {
101                    return Ok(Reclamation::Retained);
102                };
103                lease
104            }
105        };
106        let keys = Keys::new(generation);
107        let mut result = Reclamation::Retained;
108        self.mutate_owned(Some(&lease), control, &mut |read, batch| {
109            if load_state(read, generation, control)? != Some(state) {
110                if mode == ReclamationMode::Maintenance {
111                    return Ok(());
112                }
113                return Err(invalid(
114                    "generation changed during abandoned-owner acquisition",
115                ));
116            }
117            if read
118                .record_revision(keys.key(Kind::State).as_ref())?
119                .and_then(|revision| revision.observed_commit(self.owner.database))
120                .is_none()
121            {
122                return Err(invalid("abandonment requires a committed state revision"));
123            }
124            result = Reclamation::from(delete_page(
125                read,
126                batch,
127                keys,
128                State {
129                    owner: StageOwner::Leased(lease.id().allocation()),
130                    ..state
131                },
132                max_records,
133                control,
134            )?);
135            Ok(())
136        })?;
137        Ok(result)
138    }
139
140    /// Delete at most 64 payload records from a durably retired generation. The final step removes its state. Current heads and unpublished builds cannot be reclaimed here. Retained sources keep the historical pages through ordinary MVCC leases; version reclamation remains separate. Resolve an uncertain original attempt before calling again.
141    pub fn reclaim_retired_step(
142        &self,
143        generation: DiskANNGeneration,
144        max_records: usize,
145        control: &StorageReadControl,
146    ) -> StorageBackendResult<bool> {
147        control.check()?;
148        if max_records == 0 {
149            return Err(invalid("reclamation requires a positive record limit"));
150        }
151        let keys = Keys::new(generation);
152        let state_key = keys.key(Kind::State);
153        let mut complete = false;
154        self.mutate(control, &mut |read, batch| {
155            let Some(state) = load_state(read, generation, control)? else {
156                if read.contains_prefix_budgeted(keys.prefix(), control)? {
157                    return Err(invalid("generation has orphaned records"));
158                }
159                complete = true;
160                return Ok(());
161            };
162            if !matches!(
163                state.status,
164                DiskANNStageStatus::Retired | DiskANNStageStatus::Discarding
165            ) {
166                return Err(invalid("generation has no committed reclamation authority"));
167            }
168            if read
169                .record_revision(state_key.as_ref())?
170                .and_then(|revision| revision.observed_commit(self.owner.database))
171                .is_none()
172            {
173                return Err(invalid("reclamation requires a committed state revision"));
174            }
175            complete = delete_page(read, batch, keys, state, max_records, control)?;
176            Ok(())
177        })?;
178        Ok(complete)
179    }
180}
181
182impl From<bool> for Reclamation {
183    fn from(complete: bool) -> Self {
184        if complete {
185            Self::Complete
186        } else {
187            Self::More
188        }
189    }
190}
191
192/// The caller establishes authority on this exact read. State and data identity fence every deletion; no payload is loaded to select its key.
193pub(super) fn delete_page(
194    read: &dyn KeyValueRead,
195    batch: &mut dyn KeyValueBatch,
196    keys: Keys,
197    state: State,
198    max_records: usize,
199    control: &StorageReadControl,
200) -> StorageBackendResult<bool> {
201    let limit = max_records.min(KEY_PAGE_LIMIT);
202    let state_key = keys.key(Kind::State);
203    let page = key_page(read, keys, Some(state_key.as_ref()), limit, control)?;
204    batch.require_unchanged(&database_key())?;
205    batch.require_unchanged(state_key.as_ref())?;
206    for (key, _) in page.iter() {
207        control.check()?;
208        batch.delete(key.as_ref())?;
209    }
210    let complete = page.len() < limit;
211    if complete {
212        batch.delete(state_key.as_ref())?;
213    } else {
214        batch.put(
215            state_key.as_ref(),
216            &State {
217                status: DiskANNStageStatus::Discarding,
218                ..state
219            }
220            .encode(),
221        )?;
222    }
223    control.check()?;
224    Ok(complete)
225}