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::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}
43
44pub struct ExportDataOptions<'a> {
45    pub snapshot_uri: &'a str,
46    pub storage_config: &'a ObjectStoreConfig,
47    pub parallelism: usize,
48    pub chunk_parallelism: usize,
49}
50
51pub async fn export_data(
52    storage: &dyn SnapshotStorage,
53    database_client: &DatabaseClient,
54    manifest: &mut Manifest,
55    options: ExportDataOptions<'_>,
56    progress: &dyn ProgressReporter,
57) -> Result<()> {
58    if manifest.chunks.is_empty() {
59        return Ok(());
60    }
61
62    let context = ExportContext {
63        storage,
64        database_client,
65        snapshot_uri: options.snapshot_uri,
66        storage_config: options.storage_config,
67        catalog: manifest.catalog.clone(),
68        schemas: manifest.schemas.clone(),
69        format: manifest.format,
70        parallelism: options.parallelism,
71    };
72
73    // One progress unit per chunk. Already completed/skipped chunks from a
74    // previous run count up front so the reported total matches the chunk plan
75    // regardless of resume position; each chunk finalized in this run (completed,
76    // skipped, or failed) then increments exactly once.
77    let progress_phase = ProgressPhase::start(
78        progress,
79        "Export data chunks",
80        Some(manifest.chunks.len() as u64),
81    );
82    let already_done = (manifest.completed_count() + manifest.skipped_count()) as u64;
83    if already_done > 0 {
84        progress.inc(already_done);
85    }
86
87    let result = if options.chunk_parallelism <= 1 {
88        export_data_serial(&context, storage, manifest, progress).await
89    } else {
90        export_data_concurrent(
91            &context,
92            storage,
93            manifest,
94            options.chunk_parallelism,
95            progress,
96        )
97        .await
98    };
99
100    progress_phase.finish();
101    result
102}
103
104/// Exports chunks one at a time, preserving the original serial behavior.
105async fn export_data_serial(
106    context: &ExportContext<'_>,
107    storage: &dyn SnapshotStorage,
108    manifest: &mut Manifest,
109    progress: &dyn ProgressReporter,
110) -> Result<()> {
111    for idx in 0..manifest.chunks.len() {
112        if matches!(
113            manifest.chunks[idx].status,
114            ChunkStatus::Completed | ChunkStatus::Skipped
115        ) {
116            continue;
117        }
118
119        let (chunk_id, time_range) = mark_chunk_in_progress(manifest, idx);
120        manifest.touch();
121        storage.write_manifest(manifest).await?;
122
123        let export_result = export_chunk(context, chunk_id, time_range).await;
124
125        let result = match export_result {
126            Ok(files) => {
127                mark_chunk_completed(manifest, idx, files);
128                Ok(())
129            }
130            Err(err) => {
131                mark_chunk_failed(manifest, idx, err.to_string());
132                Err(err)
133            }
134        };
135
136        manifest.touch();
137        storage.write_manifest(manifest).await?;
138        // The chunk is finalized (completed, skipped, or failed) and persisted.
139        progress.inc(1);
140
141        result?;
142    }
143
144    Ok(())
145}
146
147/// Exports up to `chunk_parallelism` chunks concurrently on the client.
148///
149/// The coordinator owns all manifest mutation/persistence: it marks chunks
150/// `InProgress` and persists the manifest before polling their futures, then
151/// applies each chunk result and persists again on completion. The export
152/// futures only run COPY DATABASE and collect files; they never touch the
153/// manifest, so manifest writes stay serialized in this task.
154///
155/// On the first chunk failure we stop scheduling new chunks but let already
156/// in-flight chunks finish and persist their final status, then return the
157/// first error.
158async fn export_data_concurrent(
159    context: &ExportContext<'_>,
160    storage: &dyn SnapshotStorage,
161    manifest: &mut Manifest,
162    chunk_parallelism: usize,
163    progress: &dyn ProgressReporter,
164) -> Result<()> {
165    let mut pending = FuturesUnordered::new();
166    let mut next_idx = 0;
167    let mut first_error: Option<Error> = None;
168
169    loop {
170        let mut scheduled = false;
171
172        // Schedule eligible chunks in order up to the parallelism limit. Once a
173        // failure is seen, stop scheduling but keep draining in-flight chunks.
174        while first_error.is_none() && pending.len() < chunk_parallelism {
175            let Some(idx) = next_eligible_chunk(manifest, &mut next_idx) else {
176                break;
177            };
178
179            let (chunk_id, time_range) = mark_chunk_in_progress(manifest, idx);
180            scheduled = true;
181
182            pending.push(async move {
183                let result = export_chunk(context, chunk_id, time_range).await;
184                (idx, result)
185            });
186        }
187
188        if scheduled {
189            manifest.touch();
190            storage.write_manifest(manifest).await?;
191        }
192
193        let Some((idx, export_result)) = pending.next().await else {
194            break;
195        };
196
197        match export_result {
198            Ok(files) => mark_chunk_completed(manifest, idx, files),
199            Err(err) => {
200                mark_chunk_failed(manifest, idx, err.to_string());
201                if first_error.is_none() {
202                    first_error = Some(err);
203                }
204            }
205        }
206        manifest.touch();
207        storage.write_manifest(manifest).await?;
208        // The chunk is finalized (completed, skipped, or failed) and persisted.
209        progress.inc(1);
210    }
211
212    match first_error {
213        Some(err) => Err(err),
214        None => Ok(()),
215    }
216}
217
218/// Returns the index of the next chunk eligible for export, scanning forward
219/// from `next_idx` and skipping already Completed/Skipped chunks. Advances
220/// `next_idx` past the returned chunk so each chunk is scheduled at most once.
221fn next_eligible_chunk(manifest: &Manifest, next_idx: &mut usize) -> Option<usize> {
222    while *next_idx < manifest.chunks.len() {
223        let idx = *next_idx;
224        *next_idx += 1;
225        if !matches!(
226            manifest.chunks[idx].status,
227            ChunkStatus::Completed | ChunkStatus::Skipped
228        ) {
229            return Some(idx);
230        }
231    }
232    None
233}
234
235fn mark_chunk_in_progress(manifest: &mut Manifest, idx: usize) -> (u32, TimeRange) {
236    let chunk = &mut manifest.chunks[idx];
237    chunk.mark_in_progress();
238    (chunk.id, chunk.time_range.clone())
239}
240
241fn mark_chunk_completed(manifest: &mut Manifest, idx: usize, files: Vec<String>) {
242    let chunk = &mut manifest.chunks[idx];
243    if files.is_empty() {
244        chunk.mark_skipped();
245    } else {
246        chunk.mark_completed(files, None);
247    }
248}
249
250fn mark_chunk_failed(manifest: &mut Manifest, idx: usize, error: String) {
251    let chunk = &mut manifest.chunks[idx];
252    chunk.mark_failed(error);
253}
254
255async fn export_chunk(
256    context: &ExportContext<'_>,
257    chunk_id: u32,
258    time_range: TimeRange,
259) -> Result<Vec<String>> {
260    let scheme = StorageScheme::from_uri(context.snapshot_uri)?;
261    let needs_dir = matches!(scheme, StorageScheme::File);
262    let copy_options = CopyOptions {
263        format: context.format,
264        time_range,
265        parallelism: context.parallelism,
266    };
267
268    for schema in &context.schemas {
269        let prefix = data_dir_for_schema_chunk(schema, chunk_id);
270        if needs_dir {
271            context.storage.create_dir_all(&prefix).await?;
272        }
273
274        let target = build_copy_target(
275            context.snapshot_uri,
276            context.storage_config,
277            schema,
278            chunk_id,
279        )?;
280        execute_copy_database(
281            context.database_client,
282            &context.catalog,
283            schema,
284            &target,
285            &copy_options,
286        )
287        .await?;
288    }
289
290    let files = list_chunk_files(context.storage, &context.schemas, chunk_id).await?;
291    info!("Collected {} files for chunk {}", files.len(), chunk_id);
292    Ok(files)
293}
294
295async fn list_chunk_files(
296    storage: &dyn SnapshotStorage,
297    schemas: &[String],
298    chunk_id: u32,
299) -> Result<Vec<String>> {
300    let mut files = Vec::new();
301
302    for schema in schemas {
303        let prefix = data_dir_for_schema_chunk(schema, chunk_id);
304        files.extend(storage.list_files_recursive(&prefix).await?);
305    }
306
307    files.sort();
308    Ok(files)
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::data::export_v2::manifest::ChunkMeta;
315
316    fn pending_manifest(n: u32) -> Manifest {
317        let mut manifest = Manifest::new_full(
318            "greptime".to_string(),
319            vec!["public".to_string()],
320            TimeRange::unbounded(),
321            DataFormat::Parquet,
322        );
323        manifest.chunks = (1..=n)
324            .map(|id| ChunkMeta::new(id, TimeRange::unbounded()))
325            .collect();
326        manifest
327    }
328
329    #[test]
330    fn test_next_eligible_chunk_scans_in_order() {
331        let manifest = pending_manifest(3);
332        let mut next_idx = 0;
333
334        assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), Some(0));
335        assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), Some(1));
336        assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), Some(2));
337        assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), None);
338    }
339
340    #[test]
341    fn test_next_eligible_chunk_skips_completed_and_skipped() {
342        let mut manifest = pending_manifest(4);
343        manifest.chunks[0].mark_completed(vec!["data/public/1/f.parquet".to_string()], None);
344        manifest.chunks[2].mark_skipped();
345        let mut next_idx = 0;
346
347        // Chunk 0 (completed) and chunk 2 (skipped) are skipped; failed/pending eligible.
348        assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), Some(1));
349        assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), Some(3));
350        assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), None);
351    }
352
353    #[test]
354    fn test_next_eligible_chunk_treats_failed_and_in_progress_as_eligible() {
355        let mut manifest = pending_manifest(2);
356        manifest.chunks[0].mark_failed("boom".to_string());
357        manifest.chunks[1].mark_in_progress();
358        let mut next_idx = 0;
359
360        assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), Some(0));
361        assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), Some(1));
362        assert_eq!(next_eligible_chunk(&manifest, &mut next_idx), None);
363    }
364}