linkrs_storage/engine/graph_storage/
persistence_api.rs1use std::sync::Arc;
2
3use linkrs_core::types::{CommitLsn, CompactConfig, LabelId, Timestamp};
4use linkrs_core::{StorageError, StorageResult, Value};
5
6use crate::vertex::ShardedVertexTable;
7use crate::StoragePersistenceOps;
8
9use super::persistence;
10use super::GraphStorage;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct VertexReshardDryRun {
18 pub live_rows: usize,
19 pub new_num_shards: usize,
20 pub per_shard_rows: Vec<usize>,
21 pub edge_blockers: Vec<String>,
22}
23
24impl GraphStorage {
25 pub fn offline_inspect_vertex_store(
32 &self,
33 vertices_dir: &std::path::Path,
34 ) -> StorageResult<String> {
35 let health = ShardedVertexTable::inspect_store_health(vertices_dir)?;
36 let summary = format!(
37 "vertex store health: tables={} chain_ok={} healthy={} issues={:?}",
38 health.tables.len(),
39 health.chain_ok,
40 health.is_healthy(),
41 health.issues,
42 );
43 log::info!("{}", summary);
44 Ok(summary)
45 }
46
47 pub fn dry_run_reshard_vertex_table(
56 &self,
57 label: LabelId,
58 new_num_shards: usize,
59 ) -> StorageResult<VertexReshardDryRun> {
60 let preview = self.ctx.data_store().with_vertex_tables(|tables| {
61 let table = tables.get(&label).ok_or_else(|| {
62 StorageError::label_not_found(format!("vertex label {} not found", label))
63 })?;
64 table.dry_run_reshard_to(new_num_shards)
65 })?;
66 let blockers: Vec<String> = self.ctx.data_store().with_edge_tables(|tables| {
67 let mut blockers = Vec::new();
68 for (key, table) in tables.iter() {
69 if key.src_label != label
70 && key.dst_label != label
71 && key.src_label != 0
72 && key.dst_label != 0
73 {
74 continue;
75 }
76 if table.read().edge_count() > 0 {
77 blockers.push(format!(
78 "edge partition ({},{},{}) holds live edges",
79 key.src_label, key.dst_label, key.edge_label
80 ));
81 }
82 }
83 blockers
84 });
85 Ok(VertexReshardDryRun {
86 live_rows: preview.live_rows,
87 new_num_shards: preview.new_num_shards,
88 per_shard_rows: preview.per_shard_rows,
89 edge_blockers: blockers,
90 })
91 }
92
93 fn probe_rebuilt_vertex_baseline(
101 &self,
102 rebuilt: &ShardedVertexTable,
103 label: LabelId,
104 ) -> StorageResult<()> {
105 let Some(paths) = self.ctx.storage_paths() else {
106 return Ok(());
107 };
108 let probe_dir = paths
109 .vertices_dir()
110 .join(format!("label_{label}.reshard_probe.tmp"));
111 let _ = std::fs::remove_dir_all(&probe_dir);
112 let verdict = rebuilt
113 .flush(&probe_dir, self.ctx.flush_compression())
114 .and_then(|()| {
115 let report = ShardedVertexTable::inspect_commit_health(&probe_dir)?;
116 if report.is_healthy() {
117 return Ok(());
118 }
119 let mut issues = report.missing_files.clone();
120 issues.extend(report.pk_issues.clone());
121 issues.extend(report.lineage_issues.clone());
122 if !report.manifest_present || !report.manifest_decodable {
123 issues.push("commit manifest missing or undecodable".to_string());
124 }
125 Err(StorageError::invalid_operation(format!(
126 "reshard probe step failed: rebuilt baseline for vertex label {} \
127 is unhealthy at {}: {:?}",
128 label,
129 probe_dir.display(),
130 issues
131 )))
132 });
133 let _ = std::fs::remove_dir_all(&probe_dir);
136 verdict
137 }
138
139 fn verify_resharded_baseline(&self, label: LabelId, checkpoint_seq: u64) -> StorageResult<()> {
145 let persistence = self.ctx.persistence().as_ref().ok_or_else(|| {
146 StorageError::invalid_operation(
147 "reshard health step failed: no persistence coordinator for the checkpoint",
148 )
149 })?;
150 let table_dir = persistence
151 .read()
152 .checkpoint_dir()
153 .join(format!("checkpoint_{checkpoint_seq}"))
154 .join("data")
155 .join("vertices")
156 .join(format!("label_{label}"));
157 let report = ShardedVertexTable::inspect_commit_health(&table_dir).map_err(|e| {
158 StorageError::db_error(format!(
159 "reshard health step failed: cannot inspect the published vertex baseline \
160 for label {} at {}: {}",
161 label,
162 table_dir.display(),
163 e
164 ))
165 })?;
166 if !report.is_healthy() {
167 return Err(StorageError::invalid_operation(format!(
168 "reshard health step failed: published checkpoint {} for vertex label {} \
169 is unhealthy at {}: missing={:?} pk={:?} lineage={:?} manifest_present={} \
170 manifest_decodable={}. Old snapshots are kept; restore the previous \
171 checkpoint before retrying.",
172 checkpoint_seq,
173 label,
174 table_dir.display(),
175 report.missing_files,
176 report.pk_issues,
177 report.lineage_issues,
178 report.manifest_present,
179 report.manifest_decodable,
180 )));
181 }
182 Ok(())
183 }
184
185 pub fn offline_reshard_vertex_table(
204 &self,
205 label: LabelId,
206 new_num_shards: usize,
207 ) -> StorageResult<usize> {
208 let _barrier = self.ctx.offline_maintenance_barrier();
209 let blockers: Vec<String> = self.ctx.data_store().with_edge_tables(|tables| {
210 let mut blockers = Vec::new();
211 for (key, table) in tables.iter() {
212 if key.src_label != label
213 && key.dst_label != label
214 && key.src_label != 0
215 && key.dst_label != 0
216 {
217 continue;
218 }
219 if table.read().edge_count() > 0 {
220 blockers.push(format!(
221 "edge partition ({},{},{}) holds live edges",
222 key.src_label, key.dst_label, key.edge_label
223 ));
224 }
225 }
226 blockers
227 });
228 if !blockers.is_empty() {
229 return Err(StorageError::invalid_operation(format!(
230 "reshard refused: vertex label {} is referenced by {}: \
231 drain or migrate those edges before offline redistribution, \
232 or use the edge-aware redistribution entry",
233 label,
234 blockers.join("; ")
235 )));
236 }
237 let (rebuilt, _) = self.ctx.data_store().with_vertex_tables(|tables| {
238 let table = tables.get(&label).ok_or_else(|| {
239 StorageError::label_not_found(format!("vertex label {} not found", label))
240 })?;
241 table.reshard_to(new_num_shards)
242 })?;
243 let rows = rebuilt.approximate_total_count();
244 let shards = rebuilt.num_shards();
245 self.probe_rebuilt_vertex_baseline(&rebuilt, label)?;
248 self.ctx.data_store().with_vertex_tables_mut(|tables| {
249 tables.insert(label, Arc::new(rebuilt));
250 Ok::<(), StorageError>(())
251 })?;
252 self.ctx.invalidate_vertex_cache(label);
253 self.ctx.mark_vertex_modified(label);
254 self.ctx.bump_layout_version();
255 let stats = self.create_checkpoint()?.ok_or_else(|| {
258 StorageError::invalid_operation(format!(
259 "reshard finalize step failed: vertex label {} was rebuilt to {} shards \
260 but no checkpoint baseline was written: refusing success; the next open \
261 still sees the old baseline",
262 label, shards,
263 ))
264 })?;
265 self.verify_resharded_baseline(label, stats.checkpoint_id)?;
267 log::info!(
268 "Resharded vertex label {} to {} shards, {} rows carried over, checkpoint {} published",
269 label,
270 shards,
271 rows,
272 stats.checkpoint_id
273 );
274 self.cleanup_snapshots()?;
275 Ok(rows)
276 }
277
278 pub fn offline_reshard_vertex_table_with_edge_translation(
292 &self,
293 label: LabelId,
294 new_num_shards: usize,
295 ) -> StorageResult<usize> {
296 use std::collections::HashMap;
297 let _barrier = self.ctx.offline_maintenance_barrier();
298 let (rebuilt, id_mapping) = self.ctx.data_store().with_vertex_tables(|tables| {
299 let table = tables.get(&label).ok_or_else(|| {
300 StorageError::label_not_found(format!("vertex label {} not found", label))
301 })?;
302 table.reshard_to(new_num_shards)
303 })?;
304 let rows = rebuilt.approximate_total_count();
305 let shards = rebuilt.num_shards();
306 self.probe_rebuilt_vertex_baseline(&rebuilt, label)?;
309 self.ctx.data_store().with_vertex_tables_mut(|tables| {
310 tables.insert(label, Arc::new(rebuilt));
311 Ok::<(), StorageError>(())
312 })?;
313
314 let per_label: HashMap<LabelId, HashMap<u32, u32>> =
320 [(label, id_mapping)].into_iter().collect();
321 let mapping_for = |endpoint: LabelId| -> Option<&HashMap<u32, u32>> {
322 if let Some(mapping) = per_label.get(&endpoint) {
323 return Some(mapping);
324 }
325 if endpoint == 0 {
326 return per_label.get(&label);
327 }
328 None
329 };
330 let remapped: Vec<(crate::engine::data_store::EdgeTableKey, bool)> = self
331 .ctx
332 .data_store()
333 .for_all_edge_partitions_mut(|key, table| {
334 let src_mapping = mapping_for(key.src_label);
335 let dst_mapping = mapping_for(key.dst_label);
336 if src_mapping.is_none() && dst_mapping.is_none() {
337 return Ok((key, false));
338 }
339 table.remap_vertex_ids(src_mapping, dst_mapping)?;
340 for endpoint in [key.src_label, key.dst_label] {
341 if endpoint == label || endpoint == 0 {
342 self.ctx.mark_edge_modified(key.edge_label);
343 }
344 }
345 Ok((key, true))
346 })?;
347
348 self.ctx.invalidate_vertex_cache(label);
349 self.ctx.mark_vertex_modified(label);
350 self.ctx.bump_layout_version();
351
352 let stats = self.create_checkpoint()?.ok_or_else(|| {
355 StorageError::db_error(format!(
356 "reshard of vertex label {} to {} shards translated {} edge partition(s) \
357 but no checkpoint baseline was written: refusing success; the next open \
358 still sees the old baseline",
359 label,
360 shards,
361 remapped.iter().filter(|(_, did)| *did).count(),
362 ))
363 })?;
364 self.verify_resharded_baseline(label, stats.checkpoint_id)?;
366 log::info!(
367 "Resharded vertex label {} to {} shards with edge translation: {} rows, \
368 {} edge partition(s) translated, checkpoint {} published",
369 label,
370 shards,
371 rows,
372 remapped.iter().filter(|(_, did)| *did).count(),
373 stats.checkpoint_id,
374 );
375 self.cleanup_snapshots()?;
376 Ok(rows)
377 }
378
379 pub fn offline_bulk_import_vertices_str(
386 &self,
387 label: LabelId,
388 rows: &[(String, Vec<(Arc<str>, Value)>)],
389 ts: Timestamp,
390 sorted: bool,
391 ) -> StorageResult<usize> {
392 let table = self.ctx.data_store().with_vertex_tables(|tables| {
393 tables.get(&label).cloned().ok_or_else(|| {
394 StorageError::label_not_found(format!("vertex label {} not found", label))
395 })
396 })?;
397 let borrowed: Vec<(&str, &[(Arc<str>, Value)])> = rows
398 .iter()
399 .map(|(id, props)| (id.as_str(), props.as_slice()))
400 .collect();
401 table.bulk_import_str(&borrowed, ts, sorted)
402 }
403
404 pub fn offline_bulk_import_vertices_i64(
407 &self,
408 label: LabelId,
409 rows: &[(i64, Vec<(Arc<str>, Value)>)],
410 ts: Timestamp,
411 sorted: bool,
412 ) -> StorageResult<usize> {
413 let table = self.ctx.data_store().with_vertex_tables(|tables| {
414 tables.get(&label).cloned().ok_or_else(|| {
415 StorageError::label_not_found(format!("vertex label {} not found", label))
416 })
417 })?;
418 let borrowed: Vec<(i64, &[(Arc<str>, Value)])> = rows
419 .iter()
420 .map(|(id, props)| (*id, props.as_slice()))
421 .collect();
422 table.bulk_import_i64(&borrowed, ts, sorted)
423 }
424}
425
426impl StoragePersistenceOps for GraphStorage {
427 fn flush(&self) -> StorageResult<()> {
428 persistence::flush(&self.ctx)
429 }
430
431 fn create_checkpoint(&self) -> StorageResult<Option<crate::CheckpointStats>> {
432 persistence::create_checkpoint(&self.ctx)
433 }
434
435 fn verify_snapshot(&self, snapshot_id: u64) -> StorageResult<bool> {
436 persistence::verify_snapshot(&self.ctx, snapshot_id)
437 }
438
439 fn cleanup_snapshots(&self) -> StorageResult<usize> {
440 persistence::cleanup_snapshots(&self.ctx)
441 }
442
443 fn snapshot_stats(&self) -> crate::SnapshotStats {
444 persistence::snapshot_stats(&self.ctx)
445 }
446
447 fn persistence_diagnostics(&self) -> Option<crate::PersistenceDiagnostics> {
448 persistence::persistence_diagnostics(&self.ctx)
449 }
450
451 fn compact(&self, config: &CompactConfig) -> StorageResult<()> {
452 persistence::compact_transactional(&self.ctx, config)
453 }
454
455 fn save_data(&self) -> StorageResult<()> {
456 persistence::save_data(&self.ctx)
457 }
458
459 fn save_data_to_dir(&self, dir: &std::path::Path) -> StorageResult<()> {
460 persistence::save_data_to_dir(&self.ctx, dir)
461 }
462
463 fn auto_flush_if_needed(&self) -> StorageResult<bool> {
464 persistence::auto_flush_if_needed(&self.ctx)
465 }
466
467 fn auto_checkpoint_if_needed(&self) -> StorageResult<Option<crate::CheckpointStats>> {
468 persistence::auto_checkpoint_if_needed(&self.ctx)
469 }
470
471 fn should_flush(&self) -> bool {
472 persistence::should_flush(&self.ctx)
473 }
474
475 fn should_checkpoint(&self) -> bool {
476 persistence::should_checkpoint(&self.ctx)
477 }
478
479 fn set_outbox_materialized_lsn_provider(
480 &self,
481 provider: Arc<dyn Fn() -> StorageResult<Option<CommitLsn>> + Send + Sync>,
482 ) {
483 if let Some(persistence) = self.ctx.persistence() {
484 persistence
485 .read()
486 .set_outbox_materialized_lsn_provider(provider);
487 }
488 }
489}