1use std::collections::{HashMap, HashSet};
40use std::path::{Path, PathBuf};
41use std::sync::Arc;
42
43use super::{
44 extract_keyspace_and_table_name, extract_table_name, is_apple_double_sidecar, reader,
45 SSTableId, SSTableManager, MAX_SSTABLE_SCAN_DEPTH,
46};
47use crate::Result;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct RefreshReport {
56 pub tables_scanned: usize,
58 pub readers_added: usize,
60 pub readers_removed: usize,
62}
63
64#[derive(Debug, Clone)]
67pub(crate) enum DiscoverySource {
68 BasePath,
71 TableDirs(Vec<PathBuf>),
74}
75
76pub(crate) fn base_path_table_key(
86 path: &Path,
87 base_dir_name: &str,
88 header_table_name: &str,
89) -> Option<String> {
90 if let Some((keyspace, table_name)) = extract_keyspace_and_table_name(path) {
91 if table_name.as_str() != base_dir_name {
92 return Some(format!("{}.{}", keyspace, table_name));
93 }
94 }
95 if let Some(name) = extract_table_name(path) {
96 if name.as_str() != base_dir_name {
97 return Some(name);
98 }
99 }
100 if header_table_name != "test_table" && !header_table_name.is_empty() {
101 return Some(header_table_name.to_string());
102 }
103 None
104}
105
106pub(crate) fn table_dir_table_key(path: &Path) -> Option<String> {
110 if let Some((keyspace, table_name)) = extract_keyspace_and_table_name(path) {
111 Some(format!("{}.{}", keyspace, table_name))
112 } else {
113 extract_table_name(path)
114 }
115}
116
117fn canon(p: &Path) -> PathBuf {
121 std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf())
122}
123
124impl SSTableManager {
125 pub(crate) async fn open_reader_with_schema(
133 &self,
134 path: &Path,
135 ) -> Result<Arc<reader::SSTableReader>> {
136 #[cfg_attr(not(feature = "state_machine"), allow(unused_mut))]
137 let mut reader = reader::SSTableReader::open_with_cache(
138 path,
139 &self.config,
140 self.platform.clone(),
141 self.chunk_cache.clone(),
142 )
143 .await?;
144
145 #[cfg(feature = "state_machine")]
146 {
147 let schema_reg_guard = self.schema_registry.read().await;
148 if let Some(ref registry_rwlock) = *schema_reg_guard {
149 #[allow(deprecated)]
152 reader.set_schema_registry(Arc::clone(registry_rwlock));
153
154 reader.resolve_registry_schema().await;
159
160 let schema_registry = registry_rwlock.read().await;
162 let udt_registry_lock = schema_registry.get_udt_registry();
163 let udt_registry = udt_registry_lock.read().await.clone();
164 reader.set_udt_registry(udt_registry);
165 }
166 }
167
168 Ok(Arc::new(reader))
169 }
170
171 async fn discover_data_file_paths(&self) -> Result<Vec<PathBuf>> {
174 match &self.discovery_source {
175 DiscoverySource::BasePath => {
176 if !self.platform.fs().exists(&self.base_path).await? {
177 return Ok(Vec::new());
178 }
179 SSTableManager::find_data_files(
180 &self.platform,
181 &self.base_path,
182 MAX_SSTABLE_SCAN_DEPTH,
183 )
184 .await
185 }
186 DiscoverySource::TableDirs(dirs) => {
187 let mut out = Vec::new();
188 for dir in dirs {
189 if !self.platform.fs().exists(dir).await? {
190 continue;
191 }
192 let mut entries = match self.platform.fs().read_dir(dir).await {
193 Ok(entries) => entries,
194 Err(_) => continue,
195 };
196 while let Some(entry) = entries.next_entry().await? {
197 let path = entry.path();
198 if let Some(fname) = path.file_name().and_then(|n| n.to_str()) {
199 if fname.ends_with("-Data.db") && !is_apple_double_sidecar(fname) {
200 out.push(path);
201 }
202 }
203 }
204 }
205 Ok(out)
206 }
207 }
208 }
209
210 fn table_key_for(&self, path: &Path, reader: &reader::SSTableReader) -> Option<String> {
213 match &self.discovery_source {
214 DiscoverySource::BasePath => {
215 let base_dir_name = self
216 .base_path
217 .file_name()
218 .and_then(|n| n.to_str())
219 .unwrap_or("");
220 base_path_table_key(path, base_dir_name, &reader.header().table_name)
221 }
222 DiscoverySource::TableDirs(_) => table_dir_table_key(path),
223 }
224 }
225
226 pub async fn refresh_tables(&self) -> Result<RefreshReport> {
231 let _refresh_guard = self.refresh_lock.lock().await;
237
238 let discovered_paths = self.discover_data_file_paths().await?;
240
241 let mut canon_cache: HashMap<PathBuf, PathBuf> =
249 HashMap::with_capacity(discovered_paths.len());
250 for p in &discovered_paths {
251 canon_cache.entry(p.clone()).or_insert_with(|| canon(p));
252 }
253
254 let mut held_canon: HashSet<PathBuf> = HashSet::new();
260 {
261 let readers = self.readers.read().await;
262 let table_readers = self.table_readers.read().await;
263 for r in readers.values().chain(table_readers.values().flatten()) {
264 let c = canon_cache
265 .entry(r.file_path.clone())
266 .or_insert_with(|| canon(&r.file_path))
267 .clone();
268 held_canon.insert(c);
269 }
270 }
271
272 let canon_of = |p: &Path| -> PathBuf {
276 canon_cache
277 .get(p)
278 .cloned()
279 .unwrap_or_else(|| p.to_path_buf())
280 };
281
282 let discovered_canon: HashSet<PathBuf> =
283 discovered_paths.iter().map(|p| canon_of(p)).collect();
284
285 let added_paths: Vec<PathBuf> = discovered_paths
287 .iter()
288 .filter(|p| !held_canon.contains(&canon_of(p)))
289 .cloned()
290 .collect();
291
292 let mut opened: Vec<(
295 PathBuf,
296 SSTableId,
297 Option<String>,
298 Arc<reader::SSTableReader>,
299 )> = Vec::with_capacity(added_paths.len());
300 for path in &added_paths {
301 let Some(filename) = path.file_name().and_then(|n| n.to_str()) else {
302 continue;
303 };
304 let sstable_id = SSTableId::from_filename(filename);
305 let reader_arc = self.open_reader_with_schema(path).await?;
306 let key = self.table_key_for(path, &reader_arc);
307 opened.push((canon_of(path), sstable_id, key, reader_arc));
308 }
309
310 let mut readers = self.readers.write().await;
312 let mut table_readers = self.table_readers.write().await;
313
314 let before_canon: HashSet<PathBuf> = readers
318 .values()
319 .map(|r| canon_of(&r.file_path))
320 .chain(
321 table_readers
322 .values()
323 .flatten()
324 .map(|r| canon_of(&r.file_path)),
325 )
326 .collect();
327
328 readers.retain(|_id, r| discovered_canon.contains(&canon_of(&r.file_path)));
329 for list in table_readers.values_mut() {
330 list.retain(|r| discovered_canon.contains(&canon_of(&r.file_path)));
331 }
332 table_readers.retain(|_key, list| !list.is_empty());
333
334 let after_removal_canon: HashSet<PathBuf> = readers
335 .values()
336 .map(|r| canon_of(&r.file_path))
337 .chain(
338 table_readers
339 .values()
340 .flatten()
341 .map(|r| canon_of(&r.file_path)),
342 )
343 .collect();
344 let readers_removed = before_canon.difference(&after_removal_canon).count();
345
346 let mut readers_added = 0usize;
349 for (cpath, sstable_id, key, reader_arc) in opened {
350 if after_removal_canon.contains(&cpath) {
351 continue;
352 }
353 if readers.values().any(|r| canon_of(&r.file_path) == cpath) {
356 continue;
357 }
358 readers.insert(sstable_id, Arc::clone(&reader_arc));
359 if let Some(key) = key {
360 table_readers.entry(key).or_default().push(reader_arc);
361 }
362 readers_added += 1;
363 }
364
365 let tables_scanned = table_readers.len();
366
367 Ok(RefreshReport {
368 tables_scanned,
369 readers_added,
370 readers_removed,
371 })
372 }
373}
374
375#[cfg(all(test, feature = "write-support", feature = "state_machine"))]
376mod tests {
377 use super::*;
378 use crate::schema::{Column, KeyColumn, TableSchema};
379 use crate::storage::write_engine::{
380 CellOperation, Mutation, PartitionKey, TableId, WriteEngine, WriteEngineConfig,
381 };
382 use crate::types::Value;
383 use crate::{Config, Platform};
384 use std::collections::HashMap;
385 use tempfile::TempDir;
386
387 fn users_schema() -> TableSchema {
388 TableSchema {
389 keyspace: "ks_ptr".to_string(),
390 table: "users".to_string(),
391 partition_keys: vec![KeyColumn {
392 name: "id".to_string(),
393 data_type: "int".to_string(),
394 position: 0,
395 }],
396 clustering_keys: vec![],
397 columns: vec![
398 Column {
399 name: "id".to_string(),
400 data_type: "int".to_string(),
401 nullable: false,
402 default: None,
403 is_static: false,
404 },
405 Column {
406 name: "value".to_string(),
407 data_type: "text".to_string(),
408 nullable: true,
409 default: None,
410 is_static: false,
411 },
412 ],
413 comments: HashMap::new(),
414 dropped_columns: HashMap::new(),
415 }
416 }
417
418 async fn flush_one_partition(data_dir: &Path, wal_dir: &Path, id: i32) {
419 let config = WriteEngineConfig::new(
420 data_dir.to_path_buf(),
421 wal_dir.to_path_buf(),
422 users_schema(),
423 );
424 let mut engine = WriteEngine::new(config).expect("write engine");
425 let table_id = TableId::new("ks_ptr", "users");
426 let pk = PartitionKey::single("id", Value::Integer(id));
427 let ops = vec![CellOperation::Write {
428 column: "value".to_string(),
429 value: Value::Text(format!("v{}", id)),
430 }];
431 engine
432 .write_async(Mutation::new(
433 table_id,
434 pk,
435 None,
436 ops,
437 1_000 + id as i64,
438 None,
439 ))
440 .await
441 .expect("write");
442 engine.flush().await.expect("flush");
443 }
444
445 async fn build_two_generations(root: &Path) -> PathBuf {
448 let data_dir = root.join("data");
449 let wal_dir = root.join("wal");
450 let config = WriteEngineConfig::new(data_dir.clone(), wal_dir, users_schema());
451 let mut engine = WriteEngine::new(config).expect("write engine");
452 for id in [1_i32, 2_i32] {
453 let table_id = TableId::new("ks_ptr", "users");
454 let pk = PartitionKey::single("id", Value::Integer(id));
455 let ops = vec![CellOperation::Write {
456 column: "value".to_string(),
457 value: Value::Text(format!("v{}", id)),
458 }];
459 engine
460 .write_async(Mutation::new(
461 table_id,
462 pk,
463 None,
464 ops,
465 1_000 + id as i64,
466 None,
467 ))
468 .await
469 .expect("write");
470 engine.flush().await.expect("flush");
471 }
472 data_dir.join("ks_ptr").join("users")
473 }
474
475 fn copy_generation(src_table_dir: &Path, dst_table_dir: &Path, gen: u32) {
476 std::fs::create_dir_all(dst_table_dir).expect("mkdir dst");
477 let prefix = format!("nb-{}-big-", gen);
478 let mut copied = 0;
479 for entry in std::fs::read_dir(src_table_dir).expect("read src") {
480 let entry = entry.expect("entry");
481 let name = entry.file_name();
482 let name = name.to_str().expect("utf8");
483 if name.starts_with(&prefix) {
484 std::fs::copy(entry.path(), dst_table_dir.join(name)).expect("copy");
485 copied += 1;
486 }
487 }
488 assert!(copied > 0, "copied generation {} components", gen);
489 }
490
491 fn scan_partition_ids(
492 rows: &[(crate::RowKey, crate::ScanRow)],
493 ) -> std::collections::BTreeSet<i32> {
494 rows.iter()
495 .map(|(key, _)| {
496 let b = key.as_bytes();
497 assert_eq!(b.len(), 4, "int pk is 4 bytes");
498 i32::from_be_bytes([b[0], b[1], b[2], b[3]])
499 })
500 .collect()
501 }
502
503 #[tokio::test]
514 async fn in_flight_scan_unaffected_by_concurrent_refresh() {
515 let src = TempDir::new().expect("tmp src");
516 let src_table_dir = build_two_generations(src.path()).await;
517
518 let live = TempDir::new().expect("tmp live");
519 let live_table_dir = live.path().join("ks_ptr").join("users");
520 copy_generation(&src_table_dir, &live_table_dir, 1);
521
522 let config = Config::default();
523 let platform = Arc::new(Platform::new(&config).await.expect("platform"));
524 let manager = SSTableManager::new(
525 live.path(),
526 &config,
527 platform,
528 #[cfg(feature = "state_machine")]
529 None,
530 )
531 .await
532 .expect("manager");
533
534 let schema = users_schema();
535 let table_id = crate::types::TableId::new("ks_ptr.users");
536
537 let pre_rows = manager
540 .scan(&table_id, None, None, None, Some(&schema))
541 .await
542 .expect("pre scan");
543 assert_eq!(
544 scan_partition_ids(&pre_rows),
545 std::collections::BTreeSet::from([1]),
546 "pre-refresh scan content is gen-1 only"
547 );
548 let held_snapshot: Vec<Arc<reader::SSTableReader>> = {
549 let table_readers = manager.table_readers.read().await;
550 table_readers.values().flatten().map(Arc::clone).collect()
551 };
552 assert_eq!(
553 held_snapshot.len(),
554 1,
555 "in-flight scan holds one gen-1 reader"
556 );
557
558 copy_generation(&src_table_dir, &live_table_dir, 2);
560 let report = manager.refresh_tables().await.expect("refresh");
561 assert_eq!(report.readers_added, 1, "gen-2 added");
562
563 let post_snapshot: Vec<Arc<reader::SSTableReader>> = {
567 let table_readers = manager.table_readers.read().await;
568 table_readers.values().flatten().map(Arc::clone).collect()
569 };
570 assert_eq!(post_snapshot.len(), 2, "manager now holds both generations");
571 assert_eq!(
572 held_snapshot.len(),
573 1,
574 "the in-flight snapshot is untouched by refresh"
575 );
576 assert!(
577 post_snapshot
578 .iter()
579 .any(|r| Arc::ptr_eq(r, &held_snapshot[0])),
580 "the pre-refresh gen-1 reader Arc survives the refresh unchanged"
581 );
582 let gen2_reader = post_snapshot
583 .iter()
584 .find(|r| !Arc::ptr_eq(r, &held_snapshot[0]))
585 .expect("gen-2 reader present post-refresh");
586 assert!(
587 !Arc::ptr_eq(gen2_reader, &held_snapshot[0]),
588 "the added gen-2 reader is NOT part of the in-flight snapshot (isolation)"
589 );
590
591 let post_rows = manager
593 .scan(&table_id, None, None, None, Some(&schema))
594 .await
595 .expect("post scan");
596 assert_eq!(
597 scan_partition_ids(&post_rows),
598 std::collections::BTreeSet::from([1, 2]),
599 "post-refresh scan sees the new generation"
600 );
601 }
602
603 #[tokio::test]
607 async fn refresh_noop_preserves_reader_arc_identity() {
608 let tmp = TempDir::new().expect("tmp");
609 let data_dir = tmp.path().join("data");
610 let wal_dir = tmp.path().join("wal");
611 flush_one_partition(&data_dir, &wal_dir, 1).await;
612
613 let config = Config::default();
614 let platform = Arc::new(Platform::new(&config).await.expect("platform"));
615 let manager = SSTableManager::new(
616 &data_dir,
617 &config,
618 platform,
619 #[cfg(feature = "state_machine")]
620 None,
621 )
622 .await
623 .expect("manager");
624
625 let before: Vec<Arc<reader::SSTableReader>> = {
627 let table_readers = manager.table_readers.read().await;
628 table_readers.values().flatten().map(Arc::clone).collect()
629 };
630 assert_eq!(before.len(), 1, "one generation expected pre-refresh");
631
632 let report = manager.refresh_tables().await.expect("refresh");
633 assert_eq!(report.readers_added, 0, "no-op: nothing added");
634 assert_eq!(report.readers_removed, 0, "no-op: nothing removed");
635
636 let after: Vec<Arc<reader::SSTableReader>> = {
637 let table_readers = manager.table_readers.read().await;
638 table_readers.values().flatten().map(Arc::clone).collect()
639 };
640 assert_eq!(after.len(), 1, "still one generation after no-op refresh");
641 assert!(
642 Arc::ptr_eq(&before[0], &after[0]),
643 "no-op refresh must keep the SAME reader Arc (warm state preserved)"
644 );
645 }
646
647 #[tokio::test]
651 async fn sequential_refreshes_each_apply_correctly() {
652 let src = TempDir::new().expect("tmp src");
653 let src_table_dir = build_two_generations(src.path()).await;
654
655 let live = TempDir::new().expect("tmp live");
656 let live_table_dir = live.path().join("ks_ptr").join("users");
657 copy_generation(&src_table_dir, &live_table_dir, 1);
658
659 let config = Config::default();
660 let platform = Arc::new(Platform::new(&config).await.expect("platform"));
661 let manager = SSTableManager::new(
662 live.path(),
663 &config,
664 platform,
665 #[cfg(feature = "state_machine")]
666 None,
667 )
668 .await
669 .expect("manager");
670
671 let first = manager.refresh_tables().await.expect("first refresh");
673 assert_eq!(first.readers_added, 0, "first refresh adds nothing");
674 assert_eq!(first.readers_removed, 0, "first refresh removes nothing");
675
676 copy_generation(&src_table_dir, &live_table_dir, 2);
678 let second = manager.refresh_tables().await.expect("second refresh");
679 assert_eq!(second.readers_added, 1, "second refresh adds gen-2");
680 assert_eq!(second.readers_removed, 0, "second refresh removes nothing");
681
682 let held = {
683 let table_readers = manager.table_readers.read().await;
684 table_readers
685 .values()
686 .flatten()
687 .map(Arc::clone)
688 .collect::<Vec<_>>()
689 };
690 assert_eq!(
691 held.len(),
692 2,
693 "both generations held after the two refreshes"
694 );
695 }
696
697 #[tokio::test]
705 async fn concurrent_refreshes_serialize_no_reader_vanishes() {
706 let src = TempDir::new().expect("tmp src");
707 let src_table_dir = build_two_generations(src.path()).await;
708
709 let live = TempDir::new().expect("tmp live");
710 let live_table_dir = live.path().join("ks_ptr").join("users");
711 copy_generation(&src_table_dir, &live_table_dir, 1);
712
713 let config = Config::default();
714 let platform = Arc::new(Platform::new(&config).await.expect("platform"));
715 let manager = SSTableManager::new(
716 live.path(),
717 &config,
718 platform,
719 #[cfg(feature = "state_machine")]
720 None,
721 )
722 .await
723 .expect("manager");
724
725 copy_generation(&src_table_dir, &live_table_dir, 2);
727 let (r0, r1, r2, r3) = tokio::join!(
728 manager.refresh_tables(),
729 manager.refresh_tables(),
730 manager.refresh_tables(),
731 manager.refresh_tables(),
732 );
733 let reports = [
734 r0.expect("refresh 0"),
735 r1.expect("refresh 1"),
736 r2.expect("refresh 2"),
737 r3.expect("refresh 3"),
738 ];
739
740 let total_added: usize = reports.iter().map(|r| r.readers_added).sum();
743 let total_removed: usize = reports.iter().map(|r| r.readers_removed).sum();
744 assert_eq!(
745 total_added, 1,
746 "exactly one serialized refresh adds gen-2 (others are no-ops)"
747 );
748 assert_eq!(
749 total_removed, 0,
750 "no refresh may remove a reader a concurrent refresh just added"
751 );
752
753 let held = {
755 let table_readers = manager.table_readers.read().await;
756 table_readers
757 .values()
758 .flatten()
759 .map(Arc::clone)
760 .collect::<Vec<_>>()
761 };
762 assert_eq!(
763 held.len(),
764 2,
765 "final reader set equals on-disk truth ({{gen-1, gen-2}})"
766 );
767 let scanned = manager
768 .scan(
769 &crate::types::TableId::new("ks_ptr.users"),
770 None,
771 None,
772 None,
773 Some(&users_schema()),
774 )
775 .await
776 .expect("post scan");
777 assert_eq!(
778 scan_partition_ids(&scanned),
779 std::collections::BTreeSet::from([1, 2]),
780 "both partitions queryable after concurrent refreshes"
781 );
782 }
783}