1use 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
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}
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 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
104async 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 progress.inc(1);
140
141 result?;
142 }
143
144 Ok(())
145}
146
147async 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 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 progress.inc(1);
210 }
211
212 match first_error {
213 Some(err) => Err(err),
214 None => Ok(()),
215 }
216}
217
218fn 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 ©_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 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}