1use common_telemetry::{error, info};
16use futures::StreamExt;
17use futures::stream::FuturesUnordered;
18
19use crate::common::ObjectStoreConfig;
20use crate::data::export_v2::data::{CopyOptions, build_copy_target, execute_copy_database};
21use crate::data::export_v2::error::{Error, Result};
22use crate::data::export_v2::manifest::{ChunkStatus, DataFormat, Manifest, TimeRange};
23use crate::data::path::data_dir_for_schema_chunk;
24use crate::data::progress::{ProgressPhase, ProgressReporter};
25use crate::data::snapshot_storage::{SnapshotStorage, StorageScheme};
26use crate::database::DatabaseClient;
27
28struct ExportContext<'a> {
34 storage: &'a dyn SnapshotStorage,
35 database_client: &'a DatabaseClient,
36 snapshot_uri: &'a str,
37 storage_config: &'a ObjectStoreConfig,
38 catalog: String,
39 schemas: Vec<String>,
40 format: DataFormat,
41 parallelism: usize,
42 packed: bool,
43 resume: bool,
44}
45
46pub struct ExportDataOptions<'a> {
47 pub snapshot_uri: &'a str,
48 pub storage_config: &'a ObjectStoreConfig,
49 pub parallelism: usize,
50 pub chunk_parallelism: usize,
51 pub resume: bool,
52}
53
54pub async fn export_data(
55 storage: &dyn SnapshotStorage,
56 database_client: &DatabaseClient,
57 manifest: &mut Manifest,
58 options: ExportDataOptions<'_>,
59 progress: &dyn ProgressReporter,
60) -> Result<()> {
61 if manifest.chunks.is_empty() {
62 return Ok(());
63 }
64
65 let context = ExportContext {
66 storage,
67 database_client,
68 snapshot_uri: options.snapshot_uri,
69 storage_config: options.storage_config,
70 catalog: manifest.catalog.clone(),
71 schemas: manifest.schemas.clone(),
72 format: manifest.format,
73 packed: manifest.is_packed(),
74 parallelism: options.parallelism,
75 resume: options.resume,
76 };
77
78 let progress_phase = ProgressPhase::start(
83 progress,
84 "Export data chunks",
85 Some(manifest.chunks.len() as u64),
86 );
87 let already_done = (manifest.completed_count() + manifest.skipped_count()) as u64;
88 if already_done > 0 {
89 progress.inc(already_done);
90 }
91
92 let result = if options.chunk_parallelism <= 1 {
93 export_data_serial(&context, storage, manifest, progress).await
94 } else {
95 export_data_concurrent(
96 storage,
97 manifest,
98 options.chunk_parallelism,
99 progress,
100 |id, range| export_chunk(&context, id, range),
101 )
102 .await
103 };
104
105 progress_phase.finish();
106 result
107}
108
109async fn export_data_serial(
111 context: &ExportContext<'_>,
112 storage: &dyn SnapshotStorage,
113 manifest: &mut Manifest,
114 progress: &dyn ProgressReporter,
115) -> Result<()> {
116 for idx in 0..manifest.chunks.len() {
117 if matches!(
118 manifest.chunks[idx].status,
119 ChunkStatus::Completed | ChunkStatus::Skipped
120 ) {
121 continue;
122 }
123
124 let (chunk_id, time_range) = mark_chunk_in_progress(manifest, idx);
125 manifest.touch();
126 storage.write_manifest(manifest).await?;
127
128 let export_result = export_chunk(context, chunk_id, time_range).await;
129
130 let result = match export_result {
131 Ok(files) => {
132 mark_chunk_completed(manifest, idx, files);
133 Ok(())
134 }
135 Err(err) => {
136 mark_chunk_failed(manifest, idx, err.to_string());
137 Err(err)
138 }
139 };
140
141 manifest.touch();
142 let persistence = storage.write_manifest(manifest).await;
143 if persistence.is_ok() {
144 progress.inc(1);
145 }
146 if let Err(err) = result {
147 if let Err(secondary) = persistence {
148 error!(secondary; "Failed to persist failed export chunk");
149 }
150 return Err(err);
151 }
152 persistence?;
153 }
154
155 Ok(())
156}
157
158async fn export_data_concurrent<F, Fut>(
170 storage: &dyn SnapshotStorage,
171 manifest: &mut Manifest,
172 chunk_parallelism: usize,
173 progress: &dyn ProgressReporter,
174 export: F,
175) -> Result<()>
176where
177 F: Fn(u32, TimeRange) -> Fut,
178 Fut: std::future::Future<Output = Result<Vec<String>>>,
179{
180 let mut pending = FuturesUnordered::new();
181 let mut next_idx = 0;
182 let mut first_error: Option<Error> = None;
183
184 loop {
185 let mut admitted = Vec::new();
186 while first_error.is_none() && pending.len() + admitted.len() < chunk_parallelism {
189 let Some(idx) = next_eligible_chunk(manifest, &mut next_idx) else {
190 break;
191 };
192
193 let (chunk_id, time_range) = mark_chunk_in_progress(manifest, idx);
194 admitted.push((idx, chunk_id, time_range));
195 }
196 if !admitted.is_empty() {
197 manifest.touch();
198 match storage.write_manifest(manifest).await {
199 Ok(()) => {
200 for (idx, chunk_id, time_range) in admitted {
201 let export = &export;
202 pending.push(async move { (idx, export(chunk_id, time_range).await) });
203 }
204 }
205 Err(err) => first_error = Some(err),
206 }
207 }
208
209 let Some((idx, export_result)) = pending.next().await else {
210 break;
211 };
212
213 match export_result {
214 Ok(files) => mark_chunk_completed(manifest, idx, files),
215 Err(err) => {
216 mark_chunk_failed(manifest, idx, err.to_string());
217 if first_error.is_none() {
218 first_error = Some(err);
219 }
220 }
221 }
222 manifest.touch();
223 match storage.write_manifest(manifest).await {
224 Ok(()) => progress.inc(1),
225 Err(err) if first_error.is_none() => first_error = Some(err),
226 Err(err) => error!(err; "Failed to persist export chunk while draining"),
227 }
228 }
229
230 match first_error {
231 Some(err) => Err(err),
232 None => Ok(()),
233 }
234}
235
236fn next_eligible_chunk(manifest: &Manifest, next_idx: &mut usize) -> Option<usize> {
240 while *next_idx < manifest.chunks.len() {
241 let idx = *next_idx;
242 *next_idx += 1;
243 if !matches!(
244 manifest.chunks[idx].status,
245 ChunkStatus::Completed | ChunkStatus::Skipped
246 ) {
247 return Some(idx);
248 }
249 }
250 None
251}
252
253fn mark_chunk_in_progress(manifest: &mut Manifest, idx: usize) -> (u32, TimeRange) {
254 let chunk = &mut manifest.chunks[idx];
255 chunk.mark_in_progress();
256 (chunk.id, chunk.time_range.clone())
257}
258
259fn mark_chunk_completed(manifest: &mut Manifest, idx: usize, files: Vec<String>) {
260 let chunk = &mut manifest.chunks[idx];
261 if files.is_empty() {
262 chunk.mark_skipped();
263 } else {
264 chunk.mark_completed(files, None);
265 }
266}
267
268fn mark_chunk_failed(manifest: &mut Manifest, idx: usize, error: String) {
269 let chunk = &mut manifest.chunks[idx];
270 chunk.mark_failed(error);
271}
272
273async fn export_chunk(
274 context: &ExportContext<'_>,
275 chunk_id: u32,
276 time_range: TimeRange,
277) -> Result<Vec<String>> {
278 if context.packed {
279 context
280 .storage
281 .prepare_export_chunk(&context.schemas, chunk_id, context.resume, context.packed)
282 .await?;
283 }
284 let scheme = StorageScheme::from_uri(context.snapshot_uri)?;
285 let needs_dir = matches!(scheme, StorageScheme::File);
286 let copy_options = CopyOptions {
287 format: context.format,
288 time_range,
289 parallelism: context.parallelism,
290 packed: context.packed,
291 };
292
293 for schema in &context.schemas {
294 let prefix = data_dir_for_schema_chunk(schema, chunk_id);
295 if needs_dir {
296 context.storage.create_dir_all(&prefix).await?;
297 }
298
299 let target = build_copy_target(
300 context.snapshot_uri,
301 context.storage_config,
302 schema,
303 chunk_id,
304 )?;
305 execute_copy_database(
306 context.database_client,
307 &context.catalog,
308 schema,
309 &target,
310 ©_options,
311 )
312 .await?;
313 }
314
315 let files = list_chunk_files(context.storage, &context.schemas, chunk_id).await?;
316 if context.packed {
317 use common_datasource::packed_snapshot::{PACK_INDEX_FILE, PackIndex};
318 let mut expected = Vec::new();
319 for schema in &context.schemas {
320 let prefix = data_dir_for_schema_chunk(schema, chunk_id);
321 let index_path = format!("{prefix}{PACK_INDEX_FILE}");
322 let text = context.storage.read_text(&index_path).await?;
323 let index: PackIndex = serde_json::from_str(&text).map_err(|e| {
324 crate::data::export_v2::error::InvalidUriSnafu {
325 uri: context.snapshot_uri,
326 reason: e.to_string(),
327 }
328 .build()
329 })?;
330 index.validate().map_err(|e| {
331 crate::data::export_v2::error::InvalidUriSnafu {
332 uri: context.snapshot_uri,
333 reason: e.to_string(),
334 }
335 .build()
336 })?;
337 expected.push(index_path);
338 expected.extend(index.objects.iter().map(|o| format!("{prefix}{}", o.path)));
339 }
340 expected.sort();
341 if files != expected {
342 return crate::data::export_v2::error::InvalidUriSnafu {
343 uri: context.snapshot_uri,
344 reason: "chunk inventory differs from packed indexes",
345 }
346 .fail();
347 }
348 }
349 info!("Collected {} files for chunk {}", files.len(), chunk_id);
350 Ok(files)
351}
352
353async fn list_chunk_files(
354 storage: &dyn SnapshotStorage,
355 schemas: &[String],
356 chunk_id: u32,
357) -> Result<Vec<String>> {
358 let mut files = Vec::new();
359
360 for schema in schemas {
361 let prefix = data_dir_for_schema_chunk(schema, chunk_id);
362 files.extend(storage.list_files_recursive(&prefix).await?);
363 }
364
365 files.sort();
366 Ok(files)
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::data::export_v2::manifest::ChunkMeta;
373
374 fn pending_manifest(n: u32) -> Manifest {
375 let mut manifest = Manifest::new_full(
376 "greptime".to_string(),
377 vec!["public".to_string()],
378 TimeRange::unbounded(),
379 DataFormat::Parquet,
380 );
381 manifest.chunks = (1..=n)
382 .map(|id| ChunkMeta::new(id, TimeRange::unbounded()))
383 .collect();
384 manifest
385 }
386
387 struct FailingManifest {
388 fail_at: usize,
389 writes: std::sync::atomic::AtomicUsize,
390 release: tokio::sync::Notify,
391 }
392
393 #[async_trait::async_trait]
394 impl SnapshotStorage for FailingManifest {
395 async fn write_manifest(&self, manifest: &Manifest) -> Result<()> {
396 use std::sync::atomic::Ordering;
397
398 let write = self.writes.fetch_add(1, Ordering::SeqCst) + 1;
399 if write == 1 {
400 assert_eq!(manifest.in_progress_count(), 2);
401 }
402 if write == self.fail_at {
403 self.release.notify_one();
404 return crate::data::export_v2::error::IoSnafu {
405 operation: "injected manifest failure",
406 error: std::io::Error::other("manifest failure"),
407 }
408 .fail();
409 }
410 Ok(())
411 }
412 async fn exists(&self) -> Result<bool> {
413 unreachable!()
414 }
415 async fn read_manifest(&self) -> Result<Manifest> {
416 unreachable!()
417 }
418 async fn file_size(&self, _: &str) -> Result<Option<u64>> {
419 unreachable!()
420 }
421 async fn write_schema(
422 &self,
423 _: &crate::data::export_v2::schema::SchemaSnapshot,
424 ) -> Result<()> {
425 unreachable!()
426 }
427 async fn write_text(&self, _: &str, _: &str) -> Result<()> {
428 unreachable!()
429 }
430 async fn read_text(&self, _: &str) -> Result<String> {
431 unreachable!()
432 }
433 async fn create_dir_all(&self, _: &str) -> Result<()> {
434 unreachable!()
435 }
436 async fn list_files_recursive(&self, _: &str) -> Result<Vec<String>> {
437 unreachable!()
438 }
439 async fn delete_snapshot(&self) -> Result<()> {
440 unreachable!()
441 }
442 }
443
444 #[tokio::test]
445 async fn test_manifest_failure_stops_admission_and_drains_started_chunks() {
446 use std::sync::atomic::{AtomicUsize, Ordering};
447 for fail_at in [1, 2, 3] {
448 let storage = FailingManifest {
449 fail_at,
450 writes: AtomicUsize::new(0),
451 release: tokio::sync::Notify::new(),
452 };
453 let started = AtomicUsize::new(0);
454 let finished = AtomicUsize::new(0);
455 let second_started = tokio::sync::Notify::new();
456 let mut manifest = pending_manifest(4);
457 let result = tokio::time::timeout(
458 std::time::Duration::from_secs(5),
459 export_data_concurrent(
460 &storage,
461 &mut manifest,
462 2,
463 &crate::data::progress::NoopProgress,
464 |id, _| {
465 let (started, finished, second_started, storage) =
466 (&started, &finished, &second_started, &storage);
467 async move {
468 started.fetch_add(1, Ordering::SeqCst);
469 match id {
470 1 => second_started.notified().await,
471 2 => {
472 second_started.notify_one();
473 storage.release.notified().await;
474 }
475 _ => panic!("new chunk started after manifest persistence failed"),
476 }
477 finished.fetch_add(1, Ordering::SeqCst);
478 Ok(vec![format!("data/public/{id}/a.parquet")])
479 }
480 },
481 ),
482 )
483 .await
484 .unwrap();
485 assert!(
486 result
487 .unwrap_err()
488 .to_string()
489 .contains("injected manifest failure")
490 );
491 let expected = if fail_at == 1 { 0 } else { 2 };
492 assert_eq!(started.load(Ordering::SeqCst), expected);
493 assert_eq!(finished.load(Ordering::SeqCst), expected);
494 }
495 }
496
497 struct PausedClose {
498 inner: Option<object_store::layers::mock::oio::Writer>,
499 started: std::sync::Arc<tokio::sync::Notify>,
500 release: std::sync::Arc<tokio::sync::Notify>,
501 }
502
503 impl object_store::layers::mock::oio::Write for PausedClose {
504 async fn write(&mut self, bytes: object_store::Buffer) -> object_store::Result<()> {
505 self.inner.as_mut().unwrap().write(bytes).await
506 }
507
508 async fn close(&mut self) -> object_store::Result<object_store::layers::mock::Metadata> {
509 let mut inner = self.inner.take().unwrap();
510 let (started, release) = (self.started.clone(), self.release.clone());
511 let (inner, result) = tokio::spawn(async move {
513 started.notify_one();
514 release.notified().await;
515 let result = inner.close().await;
516 (inner, result)
517 })
518 .await
519 .unwrap();
520 self.inner = Some(inner);
521 result
522 }
523
524 async fn abort(&mut self) -> object_store::Result<()> {
525 self.inner.as_mut().unwrap().abort().await
526 }
527 }
528
529 #[tokio::test]
530 async fn packed_cancellation_drains_close_before_failing_chunk() {
531 use std::sync::Arc;
532
533 use common_datasource::packed_snapshot::PACK_INDEX_FILE;
534 use common_datasource::packed_writer::{PackedTableWriter, PackedWriter};
535 use common_datasource::parquet_writer::ParquetFileWriter;
536 use datatypes::arrow::datatypes::{DataType, Field, Schema};
537 use object_store::layers::mock::{MockLayerBuilder, MockWriterFactory};
538 use tokio_util::sync::CancellationToken;
539
540 use crate::data::snapshot_storage::OpenDalStorage;
541
542 for paused_path in ["pack-000000.bin", PACK_INDEX_FILE] {
543 let directory = tempfile::tempdir().unwrap();
544 let uri = url::Url::from_directory_path(directory.path()).unwrap();
545 let storage =
546 OpenDalStorage::from_uri(uri.as_str(), &ObjectStoreConfig::default()).unwrap();
547 let started = Arc::new(tokio::sync::Notify::new());
548 let release = Arc::new(tokio::sync::Notify::new());
549 let factory: MockWriterFactory = Arc::new({
550 let (started, release) = (started.clone(), release.clone());
551 move |path, _, inner| {
552 if path.trim_start_matches('/') == paused_path {
553 Box::new(PausedClose {
554 inner: Some(inner),
555 started: started.clone(),
556 release: release.clone(),
557 })
558 } else {
559 inner
560 }
561 }
562 });
563 let store = object_store::secure_fs::SecureFsRoot::open(directory.path())
564 .unwrap()
565 .build_operator()
566 .layer(
567 MockLayerBuilder::default()
568 .writer_factory(factory)
569 .build()
570 .unwrap(),
571 );
572 let packed = PackedWriter::new(store.clone()).unwrap();
573 let schema = Arc::new(Schema::new(vec![Field::new(
574 "value",
575 DataType::Int64,
576 true,
577 )]));
578 let mut table = ParquetFileWriter::open_packed(
579 schema,
580 store.clone(),
581 "unused",
582 None,
583 PackedTableWriter::new(packed.clone(), "empty".into(), 1, false),
584 )
585 .unwrap();
586 table.finish(None).await.unwrap();
587 let token = CancellationToken::new();
588 let mut manifest = pending_manifest(1);
589 manifest.version = 2;
590 manifest.data_layout = Some(common_datasource::packed_snapshot::PACKED_LAYOUT.into());
591 let export = export_data_concurrent(
592 &storage,
593 &mut manifest,
594 2,
595 &crate::data::progress::NoopProgress,
596 |_, _| async {
597 let mut packed = packed.lock().await;
598 let result = packed.finish(&token).await;
599 if result.is_err() {
600 packed.abort().await.unwrap();
601 }
602 result.map_err(|error| {
603 crate::data::export_v2::error::IoSnafu {
604 operation: "exporting packed chunk",
605 error: std::io::Error::other(error),
606 }
607 .build()
608 })
609 },
610 );
611 tokio::pin!(export);
612 tokio::select! {
613 _ = started.notified() => {},
614 result = &mut export => panic!("completed while close paused: {result:?}"),
615 }
616 token.cancel();
617 assert!(futures::poll!(&mut export).is_pending());
618 assert_eq!(
619 storage.read_manifest().await.unwrap().chunks[0].status,
620 ChunkStatus::InProgress
621 );
622 release.notify_one();
623 assert!(export.await.unwrap_err().to_string().contains("cancelled"));
624 let persisted = storage.read_manifest().await.unwrap();
625 assert_eq!(persisted.chunks[0].status, ChunkStatus::Failed);
626 assert!(!persisted.is_complete());
627 assert!(persisted.chunks[0].files.is_empty());
628 assert!(
629 store
630 .stat("pack-000000.bin")
631 .await
632 .unwrap()
633 .content_length()
634 > 0
635 );
636 assert_eq!(
637 store.exists(PACK_INDEX_FILE).await.unwrap(),
638 paused_path == PACK_INDEX_FILE
639 );
640 if paused_path == PACK_INDEX_FILE {
641 let index: common_datasource::packed_snapshot::PackIndex =
642 serde_json::from_slice(&store.read(PACK_INDEX_FILE).await.unwrap().to_bytes())
643 .unwrap();
644 index.validate_membership(["empty"]).unwrap();
645 }
646 }
647 }
648
649 #[test]
650 fn test_next_eligible_chunk_scans_in_order() {
651 let manifest = pending_manifest(3);
652 let mut next_idx = 0;
653
654 assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), Some(0));
655 assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), Some(1));
656 assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), Some(2));
657 assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), None);
658 }
659
660 #[test]
661 fn test_next_eligible_chunk_skips_completed_and_skipped() {
662 let mut manifest = pending_manifest(4);
663 manifest.chunks[0].mark_completed(vec!["data/public/1/f.parquet".to_string()], None);
664 manifest.chunks[2].mark_skipped();
665 let mut next_idx = 0;
666
667 assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), Some(1));
669 assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), Some(3));
670 assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), None);
671 }
672
673 #[test]
674 fn test_next_eligible_chunk_treats_failed_and_in_progress_as_eligible() {
675 let mut manifest = pending_manifest(2);
676 manifest.chunks[0].mark_failed("boom".to_string());
677 manifest.chunks[1].mark_in_progress();
678 let mut next_idx = 0;
679
680 assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), Some(0));
681 assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), Some(1));
682 assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), None);
683 }
684}