uqa_storage/key_value/diskann/
maintenance.rs1use std::sync::Arc;
10
11use uqa_core::memory::MemoryReservation;
12
13use crate::diskann_index::format::DiskANNGeneration;
14use crate::key_value::KeyValueRead;
15use crate::read_control::StorageReadControl;
16use crate::{KeyValueStore, StorageBackendResult};
17
18use super::{
19 invalid,
20 keys::{generation_prefix, state_generation, Keys, ROOT},
21 read_data_identity,
22 reclamation::{Reclamation, ReclamationMode},
23 source::KEY_PAGE_LIMIT,
24 validate_session, versioned_database, KeyValueDiskANNStore,
25};
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum DiskANNMaintenanceStatus {
29 Retained,
31 MoreRecords,
33 Reclaimed,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct DiskANNMaintenanceStep {
39 pub generation: DiskANNGeneration,
40 pub status: DiskANNMaintenanceStatus,
41}
42
43pub struct KeyValueDiskANNMaintenance {
45 repository: Option<KeyValueDiskANNStore>,
46 read: Option<Arc<dyn KeyValueRead + Send + Sync>>,
47 database: Option<[u8; 16]>,
48 after: Option<DiskANNGeneration>,
49 current: Option<DiskANNGeneration>,
50 control: StorageReadControl,
51 _memory: MemoryReservation,
52}
53
54impl KeyValueDiskANNMaintenance {
55 pub fn start(
56 store: &Arc<dyn KeyValueStore>,
57 control: &StorageReadControl,
58 ) -> StorageBackendResult<Self> {
59 control.check()?;
60 versioned_database(&**store)?;
61 let memory = control.memory().reserve(std::mem::size_of::<Self>())?;
62 let session = store.open_session_with_cancellation(control.cancellation())?;
63 validate_session(&**store, &*session)?;
64 if session.in_transaction() {
65 return Err(invalid(
66 "maintenance requires an independent inactive session",
67 ));
68 }
69 let retained = session.open_retained_read_session(control.cancellation())?;
70 validate_session(&*session, &*retained)?;
71 let mut read = None;
72 retained.with_read_view(&mut |view| {
73 read = Some(view.retain(&[ROOT])?);
74 Ok(())
75 })?;
76 let read = read.ok_or_else(|| invalid("maintenance did not retain a read boundary"))?;
77 let database = read_data_identity(&*read, control)?;
78 if database.is_none() && read.contains_prefix_budgeted(ROOT, control)? {
79 return Err(invalid("generation records have no data identity"));
80 }
81 let repository = database
82 .map(|_| KeyValueDiskANNStore::connect(&session, control))
83 .transpose()?;
84 Ok(Self {
85 repository,
86 read: database.map(|_| read),
87 database,
88 after: None,
89 current: None,
90 control: control.clone(),
91 _memory: memory,
92 })
93 }
94
95 pub fn step(&mut self) -> StorageBackendResult<Option<DiskANNMaintenanceStep>> {
97 self.control.check()?;
98 if self.current.is_none() {
99 self.current = self.next_generation()?;
100 }
101 let Some(generation) = self.current else {
102 self.read = None;
103 return Ok(None);
104 };
105 let repository = self
106 .repository
107 .as_ref()
108 .expect("discovered data has a repository");
109 let reclaimed = repository.reclaim_generation(
110 generation,
111 KEY_PAGE_LIMIT,
112 ReclamationMode::Maintenance,
113 &self.control,
114 )?;
115 let status = match reclaimed {
116 Reclamation::Complete => DiskANNMaintenanceStatus::Reclaimed,
117 Reclamation::More => DiskANNMaintenanceStatus::MoreRecords,
118 Reclamation::Retained => DiskANNMaintenanceStatus::Retained,
119 };
120 if reclaimed != Reclamation::More {
121 self.after = Some(generation);
122 self.current = None;
123 }
124 Ok(Some(DiskANNMaintenanceStep { generation, status }))
125 }
126
127 pub fn commit_pending(&self) -> StorageBackendResult<()> {
129 if let Some(repository) = &self.repository {
130 repository.commit_pending()?;
131 }
132 Ok(())
133 }
134
135 pub fn rollback_pending(&self) -> StorageBackendResult<()> {
136 if let Some(repository) = &self.repository {
137 repository.rollback_pending()?;
138 }
139 Ok(())
140 }
141
142 pub fn run(
144 store: &Arc<dyn KeyValueStore>,
145 control: &StorageReadControl,
146 ) -> StorageBackendResult<()> {
147 let mut pass = Self::start(store, control)?;
148 while pass.step()?.is_some() {}
149 super::KeyValueDiskANNMappingMaintenance::run(store, control)
150 }
151
152 fn next_generation(&self) -> StorageBackendResult<Option<DiskANNGeneration>> {
153 let Some(read) = &self.read else {
154 return Ok(None);
155 };
156 let database = self.database.expect("retained discovery data identity");
157 let after = self
158 .after
159 .map(|generation| Keys::new(generation).after_generation());
160 let mut found = None;
161 let mut failure = None;
162 read.control().check()?;
163 let source = read.visit_keys_after(
164 &generation_prefix(database),
165 after.as_ref().map(AsRef::as_ref),
166 1,
167 &self.control,
168 &mut |key| {
169 if failure.is_some() {
170 return Err(invalid("generation discovery already failed"));
171 }
172 let outcome = (|| {
173 self.control.check()?;
174 read.control().check()?;
175 if found.is_some() {
176 return Err(invalid("generation discovery exceeded its key limit"));
177 }
178 let generation = state_generation(key)?;
179 if generation.database() != database
180 || self.after.is_some_and(|previous| {
181 Keys::new(previous).prefix() >= Keys::new(generation).prefix()
182 })
183 {
184 return Err(invalid(
185 "generation discovery returned an unordered or foreign identity",
186 ));
187 }
188 found = Some(generation);
189 Ok(())
190 })();
191 if let Err(error) = outcome {
192 failure = Some(error);
193 return Err(invalid("generation discovery rejected a key"));
194 }
195 Ok(())
196 },
197 );
198 if let Some(error) = failure {
199 return Err(error);
200 }
201 source?;
202 read.control().check()?;
203 self.control.check()?;
204 Ok(found)
205 }
206}
207
208#[cfg(test)]
209mod tests;