1use linkrs_core::types::{EdgeId, Timestamp};
20use linkrs_core::{StorageError, StorageResult};
21
22use super::super::{
23 CsrBase, CsrVariant, ImmutableCsr, MutableCsrTrait, Nbr, INVALID_EDGE_ID, NO_EDGES_STORED_MSG,
24};
25use super::CsrShardSet;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum FreezeBlockReason {
33 MissingGroup,
35 NoEdgesStored,
37 AlreadyFrozen,
39}
40
41impl FreezeBlockReason {
42 pub fn message(self, gid: usize) -> String {
44 match self {
45 FreezeBlockReason::MissingGroup => {
46 format!("group {} missing, nothing to freeze", gid)
47 }
48 FreezeBlockReason::NoEdgesStored => NO_EDGES_STORED_MSG.to_string(),
49 FreezeBlockReason::AlreadyFrozen => {
50 format!("group {} is already frozen", gid)
51 }
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum FreezeFeasibility {
62 Ready {
65 edges: u64,
67 },
68 Blocked { reason: FreezeBlockReason },
70}
71
72impl FreezeFeasibility {
73 pub fn is_ready(self) -> bool {
75 matches!(self, FreezeFeasibility::Ready { .. })
76 }
77}
78
79impl CsrShardSet {
80 pub fn is_frozen(&self, gid: usize) -> bool {
82 self.shards
83 .get(&gid)
84 .is_some_and(|shard| matches!(shard.variant, CsrVariant::Frozen(_)))
85 }
86
87 pub fn is_group_frozen_for(&self, vid: u32) -> bool {
89 let Some((gid, _)) = self.route(vid) else {
90 return false;
91 };
92 self.is_frozen(gid)
93 }
94
95 pub fn freeze_group(
104 &mut self,
105 gid: usize,
106 cutoff: Timestamp,
107 reserve_ratio: f32,
108 on_edge_removed: &mut dyn FnMut(EdgeId, Timestamp),
109 ) -> StorageResult<u64> {
110 if let Err(reason) = self.check_freeze_gate(gid) {
111 return Err(StorageError::invalid_operation(reason.message(gid)));
112 }
113 self.compact_group_with_reporting(gid, cutoff, reserve_ratio, on_edge_removed);
114 let shard = self.shards.get_mut(&gid).ok_or_else(|| {
115 StorageError::invalid_operation(format!("group {} missing on freeze", gid))
116 })?;
117 let frozen = match &shard.variant {
118 CsrVariant::Multiple(csr) => ImmutableCsr::pack_from_mutable(csr),
119 CsrVariant::Single(csr) => ImmutableCsr::pack_single_from(csr),
120 CsrVariant::Pure(csr) => ImmutableCsr::pack_from_pure(csr),
125 CsrVariant::Bundled(csr) => ImmutableCsr::pack_from_bundled(csr),
126 CsrVariant::Frozen(_) | CsrVariant::None { .. } => {
127 return Err(StorageError::invalid_operation(format!(
128 "group {} changed under freeze",
129 gid
130 )));
131 }
132 };
133 let packed_edges = frozen.edge_count();
134 shard.variant = CsrVariant::Frozen(Box::new(frozen));
135 shard.mapped = None;
136 shard.dirty.deleted = true;
137 for region in shard.regions.iter_mut() {
138 region.deleted = true;
139 }
140 shard.reclaim_hint = false;
144 self.clear_group_append_log(gid);
145 Ok(packed_edges)
146 }
147
148 pub fn freeze_feasibility(&self, gid: usize) -> FreezeFeasibility {
155 match self.check_freeze_gate(gid) {
156 Ok(()) => {
157 let edges = self
158 .shards
159 .get(&gid)
160 .map(|shard| shard.variant.edge_count())
161 .unwrap_or(0);
162 FreezeFeasibility::Ready { edges }
163 }
164 Err(reason) => FreezeFeasibility::Blocked { reason },
165 }
166 }
167
168 fn check_freeze_gate(&self, gid: usize) -> Result<(), FreezeBlockReason> {
170 let Some(shard) = self.shards.get(&gid) else {
171 return Err(FreezeBlockReason::MissingGroup);
172 };
173 if shard.variant.is_writable() {
176 return Ok(());
177 }
178 if shard.variant.is_read_only() {
179 return Err(FreezeBlockReason::AlreadyFrozen);
180 }
181 Err(FreezeBlockReason::NoEdgesStored)
182 }
183
184 pub fn unfreeze_group(&mut self, gid: usize) -> StorageResult<u64> {
195 let frozen = match self.shards.get(&gid) {
196 Some(shard) => match &shard.variant {
197 CsrVariant::Frozen(csr) => csr.clone(),
198 _ => {
199 return Err(StorageError::invalid_operation(format!(
200 "group {} is not frozen",
201 gid
202 )));
203 }
204 },
205 None => {
206 return Err(StorageError::invalid_operation(format!(
207 "group {} missing, nothing to unfreeze",
208 gid
209 )));
210 }
211 };
212 let mut variant = self.fresh_variant()?;
213 let rows = frozen.vertex_capacity();
214 let mut restored_tombstones = 0usize;
215 if variant.is_bundled() {
216 let mut valued_buf: Vec<(Nbr, Option<u64>)> = Vec::new();
223 for local in 0..rows {
224 valued_buf.clear();
225 frozen.visit_physical_with_values(local as u32, |nbr, value| {
226 valued_buf.push((nbr, value));
227 true
228 });
229 for (nbr, value) in &valued_buf {
230 if nbr.edge_id == INVALID_EDGE_ID {
231 continue;
232 }
233 variant.insert_edge_with_value(
234 local as u32,
235 nbr.to_vertex_id(),
236 nbr.edge_id,
237 *value,
238 )?;
239 if nbr.delete_ts != Timestamp::MAX {
240 let deleted =
241 variant.delete_edge(local as u32, nbr.edge_id, nbr.delete_ts)?;
242 if !deleted {
243 return Err(StorageError::invalid_operation(format!(
244 "unfreeze replay missed edge {:?} in group {}",
245 nbr.edge_id, gid
246 )));
247 }
248 restored_tombstones += 1;
249 }
250 }
251 }
252 } else {
253 let mut row_buf = Vec::new();
254 for local in 0..rows {
255 frozen.fill_physical_into(local as u32, &mut row_buf);
256 for nbr in &row_buf {
257 if nbr.edge_id == INVALID_EDGE_ID {
258 continue;
259 }
260 variant.insert_edge(
261 local as u32,
262 nbr.to_vertex_id(),
263 nbr.edge_id,
264 Timestamp::MAX,
265 )?;
266 if nbr.delete_ts != Timestamp::MAX {
267 let deleted =
268 variant.delete_edge(local as u32, nbr.edge_id, nbr.delete_ts)?;
269 if !deleted {
270 return Err(StorageError::invalid_operation(format!(
271 "unfreeze replay missed edge {:?} in group {}",
272 nbr.edge_id, gid
273 )));
274 }
275 restored_tombstones += 1;
276 }
277 }
278 }
279 }
280 let restored = frozen.edge_count();
281 let shard = self.shards.get_mut(&gid).ok_or_else(|| {
282 StorageError::invalid_operation(format!("group {} missing on unfreeze", gid))
283 })?;
284 shard.variant = variant;
285 shard.mapped = None;
286 shard.dirty.deleted = true;
287 for region in shard.regions.iter_mut() {
288 region.deleted = true;
289 }
290 shard.dead_entries = restored_tombstones;
293 shard.reclaim_hint = restored_tombstones > 0;
294 self.clear_group_append_log(gid);
295 Ok(restored)
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::edge::csr_trait::CsrBase;
303 use linkrs_core::types::{EdgeStrategy, VertexId};
304
305 use super::super::DEFAULT_NODE_GROUP_BITS;
306
307 use super::super::RecordForm;
308
309 fn sample_set() -> CsrShardSet {
310 let mut set = CsrShardSet::new(
311 EdgeStrategy::Multiple,
312 DEFAULT_NODE_GROUP_BITS,
313 64,
314 RecordForm::Columnar,
315 )
316 .unwrap();
317 for src in 0..32u32 {
318 for k in 0..3u32 {
319 let dst = VertexId::edge_endpoint_key(src + k + 1, 0);
320 set.insert_edge(src, dst, EdgeId((src * 10 + k) as u64), 1)
321 .unwrap();
322 }
323 }
324 set.delete_edge(0, EdgeId(1), 5).unwrap();
325 set
326 }
327
328 #[test]
329 fn freeze_rejects_writes_and_unfreeze_restores() {
330 let mut set = sample_set();
331 let before: Vec<_> = (0..32u32).map(|src| set.physical_edges_of(src)).collect();
332 let packed = set
333 .freeze_group(0, Timestamp::MAX, 0.0, &mut |_, _| {})
334 .unwrap();
335 assert_eq!(packed, set.edge_count());
336 assert!(set.is_frozen(0));
337 assert!(set.is_group_frozen_for(3));
338 assert!(!set.is_group_frozen_for(1 << DEFAULT_NODE_GROUP_BITS));
339
340 for src in 0..32u32 {
341 assert_eq!(set.physical_edges_of(src), before[src as usize]);
342 let expected: Vec<_> = before[src as usize]
343 .iter()
344 .filter(|nbr| nbr.is_alive_at(6))
345 .copied()
346 .collect();
347 assert_eq!(set.edges_of(src, 6), expected);
348 }
349 assert!(set
350 .insert_edge(1, VertexId::edge_endpoint_key(99, 0), EdgeId(1000), 9)
351 .is_err());
352 assert!(set.delete_edge(1, EdgeId(10), 9).is_err());
353 assert_eq!(
354 set.delete_edge_by_dst(1, VertexId::edge_endpoint_key(11, 0), 9),
355 0
356 );
357
358 let restored = set.unfreeze_group(0).unwrap();
359 assert_eq!(restored, packed);
360 assert!(!set.is_frozen(0));
361 for src in 0..32u32 {
362 assert_eq!(set.physical_edges_of(src), before[src as usize]);
363 }
364 set.insert_edge(1, VertexId::edge_endpoint_key(99, 0), EdgeId(1000), 9)
365 .unwrap();
366 assert_eq!(set.edge_count(), packed + 1);
367 }
368
369 #[test]
370 fn freeze_guards_bad_states() {
371 let mut set = sample_set();
372 assert!(set
373 .freeze_group(77, Timestamp::MAX, 0.0, &mut |_, _| {})
374 .is_err());
375 set.freeze_group(0, Timestamp::MAX, 0.0, &mut |_, _| {})
376 .unwrap();
377 assert!(set
378 .freeze_group(0, Timestamp::MAX, 0.0, &mut |_, _| {})
379 .is_err());
380 assert!(set.unfreeze_group(77).is_err());
381
382 let mut none_set = CsrShardSet::new(
383 EdgeStrategy::None,
384 DEFAULT_NODE_GROUP_BITS,
385 64,
386 RecordForm::Columnar,
387 )
388 .unwrap();
389 assert!(none_set
390 .freeze_group(0, Timestamp::MAX, 0.0, &mut |_, _| {})
391 .is_err());
392
393 let mut single = CsrShardSet::new(
394 EdgeStrategy::Single,
395 DEFAULT_NODE_GROUP_BITS,
396 64,
397 RecordForm::Columnar,
398 )
399 .unwrap();
400 single
401 .insert_edge(5, VertexId::edge_endpoint_key(6, 0), EdgeId(1), 1)
402 .unwrap();
403 let packed = single
404 .freeze_group(0, Timestamp::MAX, 0.0, &mut |_, _| {})
405 .unwrap();
406 assert_eq!(packed, 1);
407 assert_eq!(
408 single.edges_of(5, 1),
409 vec![single.physical_edges_of(5).pop().unwrap()]
410 );
411 single.unfreeze_group(0).unwrap();
412 assert!(!single.is_frozen(0));
413 assert_eq!(single.edge_count(), 1);
414 }
415
416 #[test]
417 fn feasibility_matches_freeze_outcome() {
418 let mut set = sample_set();
419 let quoted = set.freeze_feasibility(0);
420 assert!(quoted.is_ready());
421 let packed = set
422 .freeze_group(0, Timestamp::MAX, 0.0, &mut |_, _| {})
423 .unwrap();
424 assert_eq!(quoted, FreezeFeasibility::Ready { edges: packed });
425
426 let blocked = set.freeze_feasibility(0);
427 assert_eq!(
428 blocked,
429 FreezeFeasibility::Blocked {
430 reason: FreezeBlockReason::AlreadyFrozen
431 }
432 );
433 assert!(!blocked.is_ready());
434 let err = set
435 .freeze_group(0, Timestamp::MAX, 0.0, &mut |_, _| {})
436 .unwrap_err();
437 assert!(err
438 .to_string()
439 .contains(&FreezeBlockReason::AlreadyFrozen.message(0)));
440
441 assert_eq!(
442 set.freeze_feasibility(77),
443 FreezeFeasibility::Blocked {
444 reason: FreezeBlockReason::MissingGroup
445 }
446 );
447 assert!(set
448 .freeze_group(77, Timestamp::MAX, 0.0, &mut |_, _| {})
449 .is_err());
450 }
451
452 #[test]
453 fn valued_bundled_groups_freeze_with_values_intact() {
454 let mut valued = CsrShardSet::new(
455 EdgeStrategy::Multiple,
456 DEFAULT_NODE_GROUP_BITS,
457 64,
458 RecordForm::Bundled,
459 )
460 .unwrap();
461 valued
462 .bundled_insert_with_value(0, VertexId::edge_endpoint_key(1, 0), EdgeId(0), 1, Some(7))
463 .unwrap();
464 valued
465 .bundled_insert_with_value(0, VertexId::edge_endpoint_key(2, 0), EdgeId(1), 1, None)
466 .unwrap();
467 assert!(valued.freeze_feasibility(0).is_ready());
468 valued
469 .freeze_group(0, Timestamp::MAX, 0.0, &mut |_, _| {})
470 .unwrap();
471 assert!(valued.is_frozen(0));
472 let frozen = valued.group_variant(0).expect("frozen group present");
474 assert_eq!(
475 frozen.bundled_value_by_edge_id(0, EdgeId(0)),
476 Some((7, true))
477 );
478 assert_eq!(
479 frozen.bundled_value_by_edge_id(0, EdgeId(1)),
480 Some((0, false))
481 );
482 let mut seen = Vec::new();
483 valued.visit_physical_with_values(0, |nbr, value| {
484 seen.push((nbr.edge_id, value));
485 true
486 });
487 assert_eq!(seen, vec![(EdgeId(0), Some(7)), (EdgeId(1), None)]);
488 valued.unfreeze_group(0).unwrap();
490 assert!(!valued.is_frozen(0));
491 let live = valued.group_variant(0).expect("live group present");
492 assert_eq!(live.bundled_value_by_edge_id(0, EdgeId(0)), Some((7, true)));
493 assert_eq!(
494 live.bundled_value_by_edge_id(0, EdgeId(1)),
495 Some((0, false))
496 );
497
498 let mut nulls = CsrShardSet::new(
500 EdgeStrategy::Multiple,
501 DEFAULT_NODE_GROUP_BITS,
502 64,
503 RecordForm::Bundled,
504 )
505 .unwrap();
506 nulls
507 .bundled_insert_with_value(0, VertexId::edge_endpoint_key(1, 0), EdgeId(0), 1, None)
508 .unwrap();
509 assert!(nulls.freeze_feasibility(0).is_ready());
510 nulls
511 .freeze_group(0, Timestamp::MAX, 0.0, &mut |_, _| {})
512 .unwrap();
513 }
514}