Skip to main content

reddb_server/storage/unified/store/
impl_native_c.rs

1use super::*;
2
3impl UnifiedStore {
4    pub fn read_native_physical_state(&self) -> Result<NativePhysicalState, StoreError> {
5        let header = self.physical_file_header().ok_or_else(|| {
6            StoreError::Serialization("native physical header is not available".to_string())
7        })?;
8
9        let collection_roots = self.read_native_collection_roots(header.collection_roots_page)?;
10        let manifest = if header.manifest_page != 0 {
11            self.read_native_manifest_summary(header.manifest_page).ok()
12        } else {
13            None
14        };
15        let registry = if header.registry_page != 0 {
16            self.read_native_registry_summary(header.registry_page).ok()
17        } else {
18            None
19        };
20        let recovery = if header.recovery_page != 0 {
21            self.read_native_recovery_summary(header.recovery_page).ok()
22        } else {
23            None
24        };
25        let catalog = if header.catalog_page != 0 {
26            self.read_native_catalog_summary(header.catalog_page).ok()
27        } else {
28            None
29        };
30        let metadata_state = if header.metadata_state_page != 0 {
31            self.read_native_metadata_state_summary(header.metadata_state_page)
32                .ok()
33        } else {
34            None
35        };
36        let vector_artifact_pages = if header.vector_artifact_page != 0 {
37            self.read_native_vector_artifact_store(header.vector_artifact_page)
38                .ok()
39        } else {
40            None
41        };
42
43        Ok(NativePhysicalState {
44            header,
45            collection_roots,
46            manifest,
47            registry,
48            recovery,
49            catalog,
50            metadata_state,
51            vector_artifact_pages,
52        })
53    }
54
55    fn read_native_blob_chain_page_ids(&self, root_page: u32) -> Result<Vec<u32>, StoreError> {
56        let Some(pager) = &self.pager else {
57            return Err(StoreError::Serialization(
58                "native blob chain requires paged mode".to_string(),
59            ));
60        };
61        if root_page == 0 {
62            return Ok(Vec::new());
63        }
64        let mut pages = Vec::new();
65        let mut current = root_page;
66        while current != 0 {
67            pages.push(current);
68            let page = pager
69                .read_page(current)
70                .map_err(|err| StoreError::Serialization(err.to_string()))?;
71            let bytes = page.as_bytes();
72            let content = &bytes[crate::storage::engine::HEADER_SIZE..];
73            let (next_page, _) = reddb_file::decode_native_blob_page(content)
74                .map_err(|err| StoreError::Serialization(err.to_string()))?;
75            current = next_page;
76        }
77        Ok(pages)
78    }
79
80    fn write_native_blob_chain(
81        &self,
82        payload: &[u8],
83        existing_root: Option<u32>,
84    ) -> Result<(u32, u32, u64), StoreError> {
85        let Some(pager) = &self.pager else {
86            return Ok((0, 0, 0));
87        };
88        if payload.is_empty() {
89            if let Some(root) = existing_root {
90                self.detached_native_blob_pages
91                    .lock()
92                    .extend(self.read_native_blob_chain_page_ids(root)?);
93            }
94            return Ok((0, 0, 0));
95        }
96
97        let chunk_capacity = reddb_file::native_blob_chunk_capacity(
98            crate::storage::engine::PAGE_SIZE,
99            crate::storage::engine::HEADER_SIZE,
100        );
101        let page_count = payload.len().div_ceil(chunk_capacity) as u32;
102        let mut page_ids = existing_root
103            .map(|root| self.read_native_blob_chain_page_ids(root))
104            .transpose()?
105            .unwrap_or_default();
106        while page_ids.len() < page_count as usize {
107            page_ids.push(
108                pager
109                    .allocate_page(crate::storage::engine::PageType::NativeMeta)
110                    .map_err(|err| StoreError::Serialization(err.to_string()))?
111                    .page_id(),
112            );
113        }
114
115        for (index, chunk) in payload.chunks(chunk_capacity).enumerate() {
116            let page_id = page_ids[index];
117            let next_page = if index + 1 < page_count as usize {
118                page_ids[index + 1]
119            } else {
120                0
121            };
122            let data = reddb_file::encode_native_blob_page(next_page, chunk);
123
124            let mut page = crate::storage::engine::Page::new(
125                crate::storage::engine::PageType::NativeMeta,
126                page_id,
127            );
128            let bytes = page.as_bytes_mut();
129            let content_start = crate::storage::engine::HEADER_SIZE;
130            let copy_len = data.len().min(bytes.len() - content_start);
131            bytes[content_start..content_start + copy_len].copy_from_slice(&data[..copy_len]);
132            pager
133                .write_page(page_id, page)
134                .map_err(|err| StoreError::Serialization(err.to_string()))?;
135        }
136
137        let root_page = page_ids[0];
138        let mut detached = self.detached_native_blob_pages.lock();
139        // A retry after an unsuccessful summary publication may retain a root
140        // previously staged for removal. Its current pages are live again.
141        for page in &page_ids[..page_count as usize] {
142            detached.remove(page);
143        }
144        detached.extend(page_ids.into_iter().skip(page_count as usize));
145
146        Ok((
147            root_page,
148            page_count,
149            crate::storage::engine::crc32(payload) as u64,
150        ))
151    }
152
153    pub fn read_native_blob_chain(&self, root_page: u32) -> Result<Vec<u8>, StoreError> {
154        let _publication = self.vault_publication_lock.lock();
155        let Some(pager) = &self.pager else {
156            return Err(StoreError::Serialization(
157                "native blob chain requires paged mode".to_string(),
158            ));
159        };
160        if root_page == 0 {
161            return Ok(Vec::new());
162        }
163        let mut current = root_page;
164        let mut payload = Vec::new();
165        while current != 0 {
166            let page = pager
167                .read_page(current)
168                .map_err(|err| StoreError::Serialization(err.to_string()))?;
169            let bytes = page.as_bytes();
170            let content = &bytes[crate::storage::engine::HEADER_SIZE..];
171            let (next_page, chunk) = reddb_file::decode_native_blob_page(content)
172                .map_err(|err| StoreError::Serialization(err.to_string()))?;
173            payload.extend_from_slice(&chunk);
174            current = next_page;
175        }
176        Ok(payload)
177    }
178
179    pub fn write_native_vector_artifact_store(
180        &self,
181        artifacts: &[(String, String, Vec<u8>)],
182        existing_page: Option<u32>,
183    ) -> Result<(u32, u64, Vec<NativeVectorArtifactPageSummary>), StoreError> {
184        let _publication = self.vault_publication_lock.lock();
185        let Some(pager) = &self.pager else {
186            return Ok((0, 0, Vec::new()));
187        };
188
189        let existing = existing_page
190            .map(|page| self.read_native_vector_artifact_store(page))
191            .transpose()?
192            .unwrap_or_default();
193        let page_id = match existing_page.filter(|page| *page != 0) {
194            Some(page) => page,
195            None => pager
196                .allocate_page(crate::storage::engine::PageType::NativeMeta)
197                .map_err(|err| StoreError::Serialization(err.to_string()))?
198                .page_id(),
199        };
200
201        let mut summaries = Vec::new();
202        for (collection, artifact_kind, bytes) in artifacts {
203            let existing_root = existing
204                .iter()
205                .find(|entry| {
206                    entry.collection == *collection && entry.artifact_kind == *artifact_kind
207                })
208                .map(|entry| entry.root_page);
209            let (root_page, page_count, checksum) =
210                self.write_native_blob_chain(bytes, existing_root)?;
211            summaries.push(NativeVectorArtifactPageSummary {
212                collection: collection.clone(),
213                artifact_kind: artifact_kind.clone(),
214                root_page,
215                page_count,
216                byte_len: bytes.len() as u64,
217                checksum,
218            });
219        }
220
221        for previous in &existing {
222            if !artifacts.iter().any(|(collection, kind, _)| {
223                previous.collection == *collection && previous.artifact_kind == *kind
224            }) {
225                self.detached_native_blob_pages
226                    .lock()
227                    .extend(self.read_native_blob_chain_page_ids(previous.root_page)?);
228            }
229        }
230
231        let data = reddb_file::encode_native_vector_artifact_store_page(&summaries);
232        let checksum = reddb_file::native_store_page_checksum(&data);
233        let mut page = crate::storage::engine::Page::new(
234            crate::storage::engine::PageType::NativeMeta,
235            page_id,
236        );
237        let bytes = page.as_bytes_mut();
238        let content_start = crate::storage::engine::HEADER_SIZE;
239        let copy_len = data.len().min(bytes.len() - content_start);
240        bytes[content_start..content_start + copy_len].copy_from_slice(&data[..copy_len]);
241        pager
242            .write_page(page_id, page)
243            .map_err(|err| StoreError::Serialization(err.to_string()))?;
244        Ok((page_id, checksum, summaries))
245    }
246
247    pub fn read_native_vector_artifact_store(
248        &self,
249        page_id: u32,
250    ) -> Result<Vec<NativeVectorArtifactPageSummary>, StoreError> {
251        let Some(pager) = &self.pager else {
252            return Err(StoreError::Serialization(
253                "native vector artifact store requires paged mode".to_string(),
254            ));
255        };
256        if page_id == 0 {
257            return Ok(Vec::new());
258        }
259        let page = pager
260            .read_page(page_id)
261            .map_err(|err| StoreError::Serialization(err.to_string()))?;
262        let bytes = page.as_bytes();
263        let content = &bytes[crate::storage::engine::HEADER_SIZE..];
264        reddb_file::decode_native_vector_artifact_store_page(content)
265            .map_err(|err| StoreError::Serialization(err.to_string()))
266    }
267
268    pub fn read_native_vector_artifact_blob(
269        &self,
270        page_id: u32,
271        collection: &str,
272        artifact_kind: Option<&str>,
273    ) -> Result<Option<(NativeVectorArtifactPageSummary, Vec<u8>)>, StoreError> {
274        let _publication = self.vault_publication_lock.lock();
275        let artifact_kind = artifact_kind.unwrap_or("hnsw");
276        let summaries = self.read_native_vector_artifact_store(page_id)?;
277        let Some(summary) = summaries.into_iter().find(|summary| {
278            summary.collection == collection && summary.artifact_kind == artifact_kind
279        }) else {
280            return Ok(None);
281        };
282        let bytes = self.read_native_blob_chain(summary.root_page)?;
283        Ok(Some((summary, bytes)))
284    }
285
286    /// Call only after the native summary and physical header have been synced.
287    /// A failed publication retains these pages; a crash before ledger admission
288    /// can leave conservative garbage, but never returns a live page for reuse.
289    pub(crate) fn publish_detached_native_blob_pages(&self) -> Result<(), StoreError> {
290        let _publication = self.vault_publication_lock.lock();
291        let Some(pager) = &self.pager else {
292            return Ok(());
293        };
294        let mut detached = self.detached_native_blob_pages.lock();
295        if detached.is_empty() {
296            return Ok(());
297        }
298        while let Some(&page) = detached.first() {
299            pager
300                .free_page(page)
301                .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
302            detached.remove(&page);
303        }
304        drop(detached);
305        // Use the existing RDF1 publication fence. Pages cannot enter the
306        // allocator until a later durable catalog forgets this quarantine.
307        self.flush_paged_registry()
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn shrinking_native_blob_does_not_read_the_previous_tail() {
317        let directory = tempfile::tempdir().expect("temporary store");
318        let store = UnifiedStore::open(&directory.path().join("blob.rdb")).expect("paged store");
319        let capacity = reddb_file::native_blob_chunk_capacity(
320            crate::storage::engine::PAGE_SIZE,
321            crate::storage::engine::HEADER_SIZE,
322        );
323        let original = vec![7; capacity * 3];
324        let (root, count, _) = store
325            .write_native_blob_chain(&original, None)
326            .expect("original blob");
327        assert_eq!(count, 3);
328        store.persist().expect("publish original blob");
329
330        let replacement = vec![9; capacity / 2];
331        let (root, count, checksum) = store
332            .write_native_blob_chain(&replacement, Some(root))
333            .expect("smaller replacement");
334        assert_eq!(count, 1);
335        let actual = store
336            .read_native_blob_chain(root)
337            .expect("read replacement");
338        assert_eq!(
339            actual.len(),
340            replacement.len(),
341            "old artifact tail survived shrink"
342        );
343        assert_eq!(actual, replacement);
344        assert_eq!(checksum, crate::storage::engine::crc32(&actual) as u64);
345    }
346
347    fn publish_artifacts(store: &UnifiedStore, artifacts: &[(String, String, Vec<u8>)]) -> u32 {
348        let mut header = store.physical_file_header().expect("physical header");
349        let previous = (header.vector_artifact_page != 0).then_some(header.vector_artifact_page);
350        let (page, checksum, _) = store
351            .write_native_vector_artifact_store(artifacts, previous)
352            .expect("write artifact summary");
353        header.vector_artifact_page = page;
354        header.vector_artifact_checksum = checksum;
355        store
356            .update_physical_file_header(header)
357            .expect("new header");
358        store.persist().expect("durable native publication");
359        store
360            .publish_detached_native_blob_pages()
361            .expect("quarantine detached pages");
362        page
363    }
364
365    #[test]
366    fn native_blob_retirement_survives_reopen_and_reuses_pages() {
367        let directory = tempfile::tempdir().expect("temporary store");
368        let path = directory.path().join("retired-blob.rdb");
369        let store = UnifiedStore::open(&path).expect("paged store");
370        let capacity = reddb_file::native_blob_chunk_capacity(
371            crate::storage::engine::PAGE_SIZE,
372            crate::storage::engine::HEADER_SIZE,
373        );
374        let large = ("rows".into(), "text.fulltext".into(), vec![7; capacity * 4]);
375        let small = ("rows".into(), "text.fulltext".into(), vec![9; capacity / 2]);
376        publish_artifacts(&store, std::slice::from_ref(&large));
377        let size = std::fs::metadata(&path).expect("allocated file").len();
378        let page = publish_artifacts(&store, std::slice::from_ref(&small));
379        let (pending, published) = store
380            .pager()
381            .expect("pager")
382            .deferred_reclamation_pages()
383            .expect("quarantine");
384        assert_eq!(pending.len(), 3, "shrunk tail must remain quarantined");
385        assert_eq!(published.len(), 3);
386        drop(store);
387
388        let store = UnifiedStore::open(&path).expect("reopen published quarantine");
389        let (summary, bytes) = store
390            .read_native_vector_artifact_blob(page, "rows", Some("text.fulltext"))
391            .expect("read artifact")
392            .expect("artifact survives");
393        assert_eq!(summary.page_count, 1);
394        assert_eq!(summary.byte_len as usize, bytes.len());
395        assert_eq!(bytes, small.2);
396        store
397            .flush_paged_registry()
398            .expect("release published tail");
399        assert!(
400            store
401                .pager()
402                .expect("pager")
403                .free_page_count()
404                .expect("free pages")
405                >= 2,
406            "one reclaimed page hosts the allocator trunk"
407        );
408        // One three-page tail may still be quarantined when the next large
409        // artifact is allocated. After that warmup, equal churn must plateau.
410        let maximum_size = size + 3 * crate::storage::engine::PAGE_SIZE as u64;
411        let mut plateau = None;
412        for cycle in 0..8 {
413            publish_artifacts(&store, std::slice::from_ref(&large));
414            publish_artifacts(&store, std::slice::from_ref(&small));
415            let current_size = std::fs::metadata(&path).expect("reused allocation").len();
416            assert!(current_size <= maximum_size);
417            let free = store
418                .pager()
419                .expect("pager")
420                .free_page_count()
421                .expect("free pages");
422            if cycle >= 2 {
423                assert_eq!(
424                    Some((current_size, free)),
425                    plateau,
426                    "equal churn must preserve allocation and free-page accounting"
427                );
428            }
429            plateau = Some((current_size, free));
430        }
431        publish_artifacts(&store, &[]);
432        store
433            .flush_paged_registry()
434            .expect("release dropped artifact");
435        assert!(store
436            .read_native_vector_artifact_store(page)
437            .expect("empty summary")
438            .is_empty());
439        assert!(store
440            .pager()
441            .expect("pager")
442            .deferred_reclamation_pages()
443            .expect("released quarantine")
444            .0
445            .is_empty());
446        // Trunk pages are also reclaimed on allocation; check actual reuse
447        // rather than counting only the free IDs stored inside those trunks.
448        publish_artifacts(&store, std::slice::from_ref(&large));
449        assert!(std::fs::metadata(&path).expect("reuse after drop").len() <= maximum_size);
450        assert_eq!(
451            store
452                .read_native_vector_artifact_blob(page, "rows", Some("text.fulltext"))
453                .expect("recreated artifact")
454                .expect("artifact exists")
455                .1,
456            large.2
457        );
458    }
459
460    #[test]
461    fn retained_blob_retry_cancels_its_unpublished_retirement() {
462        let directory = tempfile::tempdir().expect("temporary store");
463        let store =
464            UnifiedStore::open(&directory.path().join("retry-blob.rdb")).expect("paged store");
465        let bytes = vec![42; 1024];
466        let (root, _, _) = store
467            .write_native_blob_chain(&bytes, None)
468            .expect("original blob");
469        store
470            .write_native_blob_chain(&[], Some(root))
471            .expect("stage removal");
472        store
473            .write_native_blob_chain(&bytes, Some(root))
474            .expect("retain on retry");
475        store.persist().expect("retry publication");
476        store
477            .publish_detached_native_blob_pages()
478            .expect("no live retirement");
479        store.flush_paged_registry().expect("later publication");
480        assert_eq!(
481            store.read_native_blob_chain(root).expect("retained page"),
482            bytes
483        );
484        assert_eq!(
485            store
486                .pager()
487                .expect("pager")
488                .free_page_count()
489                .expect("no live free"),
490            0
491        );
492    }
493}