Skip to main content

reddb_server/physical/
metadata_file.rs

1use super::*;
2use crate::api::REDDB_FORMAT_VERSION;
3
4impl PhysicalMetadataFile {
5    pub fn from_state(
6        options: RedDBOptions,
7        catalog: CatalogSnapshot,
8        collection_roots: BTreeMap<String, u64>,
9        indexes: Vec<PhysicalIndexState>,
10        previous: Option<&PhysicalMetadataFile>,
11    ) -> Self {
12        let now = unix_ms_now();
13        let mut manifest = SchemaManifest::now(options, catalog.total_collections);
14        if let Some(previous) = previous {
15            manifest.created_at_unix_ms = previous.manifest.created_at_unix_ms;
16        }
17        manifest.updated_at_unix_ms = now;
18
19        let sequence = previous
20            .map(|previous| previous.superblock.sequence.saturating_add(1))
21            .unwrap_or(1);
22        let mut manifest_events = previous
23            .map(|previous| previous.manifest_events.clone())
24            .unwrap_or_default();
25        manifest_events.extend(build_manifest_events(
26            previous.map(|previous| &previous.superblock.collection_roots),
27            &collection_roots,
28            sequence,
29        ));
30        trim_manifest_history(&mut manifest_events);
31
32        let superblock = SuperblockHeader {
33            format_version: REDDB_FORMAT_VERSION,
34            sequence,
35            copies: DEFAULT_SUPERBLOCK_COPIES,
36            manifest: previous
37                .map(|previous| previous.superblock.manifest.clone())
38                .unwrap_or_default(),
39            free_set: previous
40                .map(|previous| previous.superblock.free_set)
41                .unwrap_or_default(),
42            collection_roots,
43        };
44
45        let mut snapshots = previous
46            .map(|previous| previous.snapshots.clone())
47            .unwrap_or_default();
48        snapshots.push(SnapshotDescriptor {
49            snapshot_id: sequence,
50            created_at_unix_ms: now,
51            superblock_sequence: sequence,
52            collection_count: catalog.total_collections,
53            total_entities: catalog.total_entities,
54        });
55        trim_snapshot_history(&mut snapshots, manifest.options.snapshot_retention);
56
57        Self {
58            protocol_version: PHYSICAL_METADATA_PROTOCOL_VERSION.to_string(),
59            generated_at_unix_ms: now,
60            last_loaded_from: previous.and_then(|previous| previous.last_loaded_from.clone()),
61            last_healed_at_unix_ms: previous.and_then(|previous| previous.last_healed_at_unix_ms),
62            manifest,
63            catalog,
64            manifest_events,
65            indexes,
66            graph_projections: previous
67                .map(|previous| previous.graph_projections.clone())
68                .unwrap_or_default(),
69            analytics_jobs: previous
70                .map(|previous| previous.analytics_jobs.clone())
71                .unwrap_or_default(),
72            tree_definitions: previous
73                .map(|previous| previous.tree_definitions.clone())
74                .unwrap_or_default(),
75            collection_ttl_defaults_ms: previous
76                .map(|previous| previous.collection_ttl_defaults_ms.clone())
77                .unwrap_or_default(),
78            collection_contracts: previous
79                .map(|previous| previous.collection_contracts.clone())
80                .unwrap_or_default(),
81            hypertables: previous
82                .map(|previous| previous.hypertables.clone())
83                .unwrap_or_default(),
84            exports: previous
85                .map(|previous| previous.exports.clone())
86                .unwrap_or_default(),
87            superblock,
88            snapshots,
89        }
90    }
91
92    pub fn metadata_path_for(data_path: &Path) -> PathBuf {
93        reddb_file::layout::physical_metadata_json_path(data_path)
94    }
95
96    pub fn metadata_binary_path_for(data_path: &Path) -> PathBuf {
97        reddb_file::layout::physical_metadata_binary_path(data_path)
98    }
99
100    pub fn metadata_journal_path_for(data_path: &Path, sequence: u64) -> PathBuf {
101        reddb_file::layout::physical_metadata_journal_path(data_path, sequence)
102    }
103
104    pub fn export_data_path_for(data_path: &Path, name: &str) -> PathBuf {
105        reddb_file::layout::physical_export_data_path(data_path, name)
106    }
107
108    pub fn load_for_data_path(data_path: &Path) -> io::Result<Self> {
109        Self::load_for_data_path_with_source(data_path).map(|(metadata, _)| metadata)
110    }
111
112    pub fn load_for_data_path_with_source(
113        data_path: &Path,
114    ) -> io::Result<(Self, PhysicalMetadataSource)> {
115        Self::load_for_data_path_with_repair(data_path, true)
116    }
117
118    /// Inspect available metadata without repairing or publishing any file.
119    pub fn inspect_for_data_path(data_path: &Path) -> io::Result<Self> {
120        Self::inspect_for_data_path_with_source(data_path).map(|(metadata, _)| metadata)
121    }
122
123    pub fn inspect_for_data_path_with_source(
124        data_path: &Path,
125    ) -> io::Result<(Self, PhysicalMetadataSource)> {
126        Self::load_for_data_path_with_repair(data_path, false)
127    }
128
129    fn load_for_data_path_with_repair(
130        data_path: &Path,
131        repair_primary: bool,
132    ) -> io::Result<(Self, PhysicalMetadataSource)> {
133        let binary_path = Self::metadata_binary_path_for(data_path);
134        if binary_path.exists() {
135            match Self::load_from_binary_path(&binary_path) {
136                Ok(metadata) => {
137                    return Ok((metadata, PhysicalMetadataSource::Binary));
138                }
139                Err(error) if is_collection_contract_decode_error(&error) => return Err(error),
140                Err(_) => {
141                    let mut journal_paths = Self::journal_paths_for_data_path(data_path)?;
142                    journal_paths.reverse();
143                    for journal_path in journal_paths {
144                        match Self::load_from_binary_path(&journal_path) {
145                            Ok(metadata) => {
146                                if !repair_primary {
147                                    return Ok((metadata, PhysicalMetadataSource::BinaryJournal));
148                                }
149                                let healed =
150                                    metadata.mark_recovery(PhysicalMetadataSource::BinaryJournal);
151                                let _ = healed.heal_primary_metadata_for_data_path(data_path);
152                                return Ok((healed, PhysicalMetadataSource::BinaryJournal));
153                            }
154                            Err(error) if is_collection_contract_decode_error(&error) => {
155                                return Err(error)
156                            }
157                            Err(_) => {}
158                        }
159                    }
160                }
161            }
162        }
163        Self::load_from_path(&Self::metadata_path_for(data_path)).map(|metadata| {
164            if !repair_primary {
165                return (metadata, PhysicalMetadataSource::Json);
166            }
167            let healed = metadata.mark_recovery(PhysicalMetadataSource::Json);
168            let _ = healed.heal_primary_metadata_for_data_path(data_path);
169            (healed, PhysicalMetadataSource::Json)
170        })
171    }
172
173    pub fn save_for_data_path(&self, data_path: &Path) -> io::Result<PathBuf> {
174        let binary_path = Self::metadata_binary_path_for(data_path);
175        if binary_path.exists() && super::seqn_journal_enabled() {
176            let sequence = Self::load_from_binary_path(&binary_path)
177                .map(|metadata| metadata.superblock.sequence)
178                .unwrap_or(self.superblock.sequence);
179            let _ = reddb_file::copy_physical_metadata_binary_to_journal(
180                data_path,
181                &binary_path,
182                sequence,
183            );
184        }
185        self.save_to_binary_path(&binary_path)?;
186        self.prune_journal_for_data_path(data_path)?;
187        if super::meta_json_sidecar_enabled() {
188            let json_path = Self::metadata_path_for(data_path);
189            self.save_to_path(&json_path)?;
190        }
191        Ok(binary_path)
192    }
193
194    pub fn load_from_path(path: &Path) -> io::Result<Self> {
195        let text = reddb_file::read_physical_metadata_document(path).map_err(|err| {
196            io::Error::new(
197                io::ErrorKind::InvalidData,
198                format!("invalid physical metadata JSON: {err}"),
199            )
200        })?;
201        Self::from_document_json(&text, "invalid physical metadata JSON")
202    }
203
204    pub fn save_to_path(&self, path: &Path) -> io::Result<()> {
205        let text = self.to_document_json(true)?;
206        reddb_file::write_physical_metadata_json_document(path, &text)
207            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))
208    }
209
210    pub fn load_from_binary_path(path: &Path) -> io::Result<Self> {
211        let text = reddb_file::read_physical_metadata_document(path).map_err(|err| {
212            io::Error::new(
213                io::ErrorKind::InvalidData,
214                format!("invalid physical metadata binary: {err}"),
215            )
216        })?;
217        Self::from_document_json(&text, "invalid physical metadata binary")
218    }
219
220    pub fn save_to_binary_path(&self, path: &Path) -> io::Result<()> {
221        let text = self.to_document_json(false)?;
222        reddb_file::write_physical_metadata_binary_document(path, &text)
223            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))
224    }
225
226    pub fn journal_paths_for_data_path(data_path: &Path) -> io::Result<Vec<PathBuf>> {
227        reddb_file::list_physical_metadata_journal_paths(data_path)
228            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))
229    }
230
231    pub fn prune_journal_for_data_path(&self, data_path: &Path) -> io::Result<()> {
232        let retention = super::seqn_journal_retention();
233        reddb_file::prune_physical_metadata_journal_paths(data_path, retention)
234            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))
235    }
236
237    pub fn heal_primary_metadata_for_data_path(&self, data_path: &Path) -> io::Result<()> {
238        let binary_path = Self::metadata_binary_path_for(data_path);
239        self.save_to_binary_path(&binary_path)?;
240        if super::meta_json_sidecar_enabled() {
241            let json_path = Self::metadata_path_for(data_path);
242            self.save_to_path(&json_path)?;
243        }
244        Ok(())
245    }
246
247    pub fn mark_recovery(&self, source: PhysicalMetadataSource) -> Self {
248        let mut metadata = self.clone();
249        metadata.last_loaded_from = Some(source.as_str().to_string());
250        metadata.last_healed_at_unix_ms = Some(unix_ms_now());
251        metadata
252    }
253
254    pub fn to_json_value(&self) -> JsonValue {
255        let json = self
256            .to_document_json(false)
257            .expect("physical metadata must encode as JSON");
258        crate::json::from_str::<JsonValue>(&json)
259            .expect("reddb-file emitted JSON the server can parse")
260    }
261
262    pub fn from_json_value(value: &JsonValue) -> io::Result<Self> {
263        Self::from_document_json(&value.to_string_compact(), "invalid physical metadata JSON")
264    }
265
266    fn to_document_json(&self, pretty: bool) -> io::Result<String> {
267        reddb_file::encode_physical_metadata_document_root_json(
268            &self.to_document_envelope(),
269            pretty,
270        )
271        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))
272    }
273
274    fn from_document_json(text: &str, context: &'static str) -> io::Result<Self> {
275        let envelope =
276            reddb_file::decode_physical_metadata_document_root_json(text).map_err(|err| {
277                io::Error::new(io::ErrorKind::InvalidData, format!("{context}: {err}"))
278            })?;
279        Self::from_document_envelope(envelope)
280    }
281
282    fn to_document_envelope(&self) -> reddb_file::PhysicalMetadataDocumentEnvelope {
283        reddb_file::PhysicalMetadataDocumentEnvelope {
284            protocol_version: self.protocol_version.clone(),
285            generated_at_unix_ms: self.generated_at_unix_ms,
286            last_loaded_from: self.last_loaded_from.clone(),
287            last_healed_at_unix_ms: self.last_healed_at_unix_ms,
288            manifest_json: manifest_to_json(&self.manifest).to_string_compact(),
289            catalog_json: catalog_to_json(&self.catalog).to_string_compact(),
290            manifest_events_json: self
291                .manifest_events
292                .iter()
293                .map(|event| manifest_event_to_json(event).to_string_compact())
294                .collect(),
295            indexes_json: self
296                .indexes
297                .iter()
298                .map(|index| index_state_to_json(index).to_string_compact())
299                .collect(),
300            graph_projections_json: self
301                .graph_projections
302                .iter()
303                .map(|projection| graph_projection_to_json(projection).to_string_compact())
304                .collect(),
305            analytics_jobs_json: self
306                .analytics_jobs
307                .iter()
308                .map(|job| analytics_job_to_json(job).to_string_compact())
309                .collect(),
310            tree_definitions_json: self
311                .tree_definitions
312                .iter()
313                .map(|definition| tree_definition_to_json(definition).to_string_compact())
314                .collect(),
315            collection_ttl_defaults_ms: self.collection_ttl_defaults_ms.clone(),
316            collection_contracts_json: self
317                .collection_contracts
318                .iter()
319                .map(|contract| collection_contract_to_json(contract).to_string_compact())
320                .collect(),
321            hypertables_json: self
322                .hypertables
323                .iter()
324                .map(|hypertable| hypertable_to_json(hypertable).to_string_compact())
325                .collect(),
326            exports_json: self
327                .exports
328                .iter()
329                .map(|export| export_descriptor_to_json(export).to_string_compact())
330                .collect(),
331            superblock_json: superblock_to_json(&self.superblock).to_string_compact(),
332            snapshots_json: self
333                .snapshots
334                .iter()
335                .map(|snapshot| snapshot_descriptor_to_json(snapshot).to_string_compact())
336                .collect(),
337        }
338    }
339
340    fn from_document_envelope(
341        envelope: reddb_file::PhysicalMetadataDocumentEnvelope,
342    ) -> io::Result<Self> {
343        Ok(Self {
344            protocol_version: envelope.protocol_version,
345            generated_at_unix_ms: envelope.generated_at_unix_ms,
346            last_loaded_from: envelope.last_loaded_from,
347            last_healed_at_unix_ms: envelope.last_healed_at_unix_ms,
348            manifest: manifest_from_json(&parse_document_fragment(
349                &envelope.manifest_json,
350                "manifest",
351            )?)?,
352            catalog: catalog_from_json(&parse_document_fragment(
353                &envelope.catalog_json,
354                "catalog",
355            )?)?,
356            manifest_events: parse_document_fragments(
357                &envelope.manifest_events_json,
358                "manifest_events",
359                manifest_event_from_json,
360            )?,
361            indexes: parse_document_fragments(
362                &envelope.indexes_json,
363                "indexes",
364                index_state_from_json,
365            )?,
366            graph_projections: parse_document_fragments(
367                &envelope.graph_projections_json,
368                "graph_projections",
369                graph_projection_from_json,
370            )?,
371            analytics_jobs: parse_document_fragments(
372                &envelope.analytics_jobs_json,
373                "analytics_jobs",
374                analytics_job_from_json,
375            )?,
376            tree_definitions: parse_document_fragments(
377                &envelope.tree_definitions_json,
378                "tree_definitions",
379                tree_definition_from_json,
380            )?,
381            collection_ttl_defaults_ms: envelope.collection_ttl_defaults_ms,
382            collection_contracts: parse_document_fragments(
383                &envelope.collection_contracts_json,
384                "collection_contracts",
385                collection_contract_from_json,
386            )?,
387            hypertables: parse_document_fragments(
388                &envelope.hypertables_json,
389                "hypertables",
390                hypertable_from_json,
391            )?,
392            exports: parse_document_fragments(
393                &envelope.exports_json,
394                "exports",
395                export_descriptor_from_json,
396            )?,
397            superblock: superblock_from_json(&parse_document_fragment(
398                &envelope.superblock_json,
399                "superblock",
400            )?)?,
401            snapshots: parse_document_fragments(
402                &envelope.snapshots_json,
403                "snapshots",
404                snapshot_descriptor_from_json,
405            )?,
406        })
407    }
408}
409
410fn parse_document_fragment(json: &str, field: &'static str) -> io::Result<JsonValue> {
411    crate::json::from_str::<JsonValue>(json)
412        .map_err(|err| invalid_data(format!("invalid physical metadata field '{field}': {err}")))
413}
414
415fn parse_document_fragments<T>(
416    fragments: &[String],
417    field: &'static str,
418    decode: fn(&JsonValue) -> io::Result<T>,
419) -> io::Result<Vec<T>> {
420    fragments
421        .iter()
422        .map(|fragment| decode(&parse_document_fragment(fragment, field)?))
423        .collect()
424}