Skip to main content

cli/data/export_v2/
coordinator.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
28/// Owned, manifest-independent context shared by all chunk export futures.
29///
30/// `catalog`/`schemas`/`format` are cloned from the manifest up front so the
31/// export futures never borrow the manifest, leaving the coordinator free to
32/// mutate and persist it while chunks are in flight.
33struct 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    // One progress unit per chunk. Already completed/skipped chunks from a
79    // previous run count up front so the reported total matches the chunk plan
80    // regardless of resume position; each chunk finalized in this run (completed,
81    // skipped, or failed) then increments exactly once.
82    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
109/// Exports chunks one at a time, preserving the original serial behavior.
110async 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
158/// Exports up to `chunk_parallelism` chunks concurrently on the client.
159///
160/// The coordinator owns all manifest mutation/persistence: it marks chunks
161/// `InProgress` and persists the manifest before polling their futures, then
162/// applies each chunk result and persists again on completion. The export
163/// futures only run COPY DATABASE and collect files; they never touch the
164/// manifest, so manifest writes stay serialized in this task.
165///
166/// On the first chunk failure we stop scheduling new chunks but let already
167/// in-flight chunks finish and persist their final status, then return the
168/// first error.
169async 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        // Schedule eligible chunks in order up to the parallelism limit. Once a
187        // failure is seen, stop scheduling but keep draining in-flight chunks.
188        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
236/// Returns the index of the next chunk eligible for export, scanning forward
237/// from `next_idx` and skipping already Completed/Skipped chunks. Advances
238/// `next_idx` past the returned chunk so each chunk is scheduled at most once.
239fn 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            &copy_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            // Model blocking storage I/O that survives dropping its caller.
512            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        // Chunk 0 (completed) and chunk 2 (skipped) are skipped; failed/pending eligible.
668        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}