1use std::collections::{BTreeMap, HashSet};
19use std::future::Future;
20
21use common_datasource::file_format::Format;
22use common_datasource::object_store::{FILE_SCHEMA, FS_SCHEMA, build_backend_for_write, parse_url};
23use common_meta::key::table_route::TableRouteValue;
24use futures::StreamExt;
25use futures::stream::FuturesUnordered;
26use session::context::QueryContextRef;
27use snafu::{OptionExt, ResultExt, ensure};
28use store_api::metric_engine_consts::{LOGICAL_TABLE_METADATA_KEY, METRIC_ENGINE_NAME};
29use table::TableRef;
30use table::metadata::TableType;
31use table::requests::{CopyDatabaseRequest, CopyDirection, CopyTableRequest};
32use tokio_util::sync::CancellationToken;
33
34use crate::error::{self, InvalidDatabaseExportSnafu, Result};
35use crate::statement::StatementExecutor;
36use crate::statement::database_copy::{
37 DatabaseExportFile, parse_parallelism_from_option_map, validate_database_directory,
38 validate_database_export_layout,
39};
40use crate::statement::export_logical_tables::{LogicalTableExport, LogicalTableExportLimits};
41
42pub struct PreparedDatabaseExport {
44 request: CopyDatabaseRequest,
45 jobs: Vec<DatabaseExportJob>,
46}
47
48impl PreparedDatabaseExport {
49 #[cfg(feature = "testing")]
51 pub fn job_count_for_test(&self) -> usize {
52 self.jobs.len()
53 }
54}
55
56enum DatabaseExportJob {
57 Ordinary {
58 table: TableRef,
59 output: DatabaseExportFile,
60 },
61 Metric(LogicalTableExport),
62}
63
64impl StatementExecutor {
65 pub async fn prepare_database_export(
68 &self,
69 req: CopyDatabaseRequest,
70 tables: Vec<TableRef>,
71 ) -> Result<PreparedDatabaseExport> {
72 validate_database_export_layout(&req.with)?;
73 validate_database_directory(&req.location)?;
74 let format = Format::try_from(&req.with).context(error::ParseFileFormatSnafu)?;
75 ensure!(
76 matches!(format, Format::Parquet(_)),
77 error::UnsupportedFormatSnafu { format }
78 );
79 let (scheme, _, _) = parse_url(&req.location).context(error::BuildBackendSnafu)?;
80 let local =
81 scheme.eq_ignore_ascii_case(FS_SCHEMA) || scheme.eq_ignore_ascii_case(FILE_SCHEMA);
82 let mut filenames = HashSet::new();
83 let mut logical = Vec::new();
84 let mut jobs = Vec::new();
85 for table in tables {
86 let info = table.table_info();
87 let name = &info.name;
88 let output = DatabaseExportFile::new(&req.location, name, ".parquet")?;
89 ensure!(
90 filenames.insert(if local {
91 output.path.to_ascii_lowercase()
92 } else {
93 output.path.clone()
94 }),
95 InvalidDatabaseExportSnafu {
96 reason: format!("duplicate output name: {name}")
97 }
98 );
99 ensure!(
100 info.catalog_name == req.catalog_name
101 && info.schema_name == req.schema_name
102 && table.table_type() == TableType::Base,
103 InvalidDatabaseExportSnafu {
104 reason: "expected base tables in the selected schema"
105 }
106 );
107 if info.meta.engine == METRIC_ENGINE_NAME {
108 ensure!(
109 info.meta
110 .options
111 .extra_options
112 .contains_key(LOGICAL_TABLE_METADATA_KEY),
113 InvalidDatabaseExportSnafu {
114 reason: "expected a Metric logical table"
115 }
116 );
117 logical.push(table);
118 } else {
119 jobs.push(DatabaseExportJob::Ordinary { table, output });
120 }
121 }
122 let ids = logical
123 .iter()
124 .map(|t| t.table_info().table_id())
125 .collect::<Vec<_>>();
126 let routes = self
127 .table_metadata_manager
128 .table_route_manager()
129 .table_route_storage()
130 .batch_get(&ids)
131 .await
132 .context(error::TableMetadataManagerSnafu)?;
133 let mut groups = BTreeMap::<_, Vec<TableRef>>::new();
134 for (table, route) in logical.into_iter().zip(routes) {
135 let Some(TableRouteValue::Logical(route)) = route else {
136 return InvalidDatabaseExportSnafu {
137 reason: format!(
138 "missing or non-logical route for {}",
139 table.table_info().table_id()
140 ),
141 }
142 .fail();
143 };
144 groups
145 .entry(route.physical_table_id())
146 .or_default()
147 .push(table);
148 }
149 let physical_ids = groups.keys().copied().collect::<Vec<_>>();
150 let mut physical = self
151 .catalog_manager
152 .tables_by_ids(&req.catalog_name, &req.schema_name, &physical_ids)
153 .await
154 .context(error::CatalogSnafu)?
155 .into_iter()
156 .map(|t| (t.table_info().table_id(), t))
157 .collect::<BTreeMap<_, _>>();
158 for (id, tables) in groups {
159 let table = physical
160 .remove(&id)
161 .with_context(|| InvalidDatabaseExportSnafu {
162 reason: format!("missing physical table {id} in the selected schema"),
163 })?;
164 jobs.push(DatabaseExportJob::Metric(
165 LogicalTableExport::try_new_in_directory(table, &tables, &req.location)?,
166 ));
167 }
168 build_backend_for_write(&req.location, &req.connection, &self.local_file_access)
169 .await
170 .context(error::BuildBackendSnafu)?;
171 Ok(PreparedDatabaseExport { request: req, jobs })
172 }
173}
174
175#[derive(Debug)]
177pub struct DatabaseExportSummary {
178 pub rows: usize,
179 pub output_files: Vec<String>,
180}
181
182impl StatementExecutor {
183 pub async fn export_database(
187 &self,
188 plan: PreparedDatabaseExport,
189 cancellation: &CancellationToken,
190 ctx: QueryContextRef,
191 ) -> Result<DatabaseExportSummary> {
192 let mut output_files = Vec::new();
193 for job in &plan.jobs {
194 match job {
195 DatabaseExportJob::Ordinary { output, .. } => {
196 output_files.push(output.location.clone())
197 }
198 DatabaseExportJob::Metric(unit) => {
199 output_files.extend(unit.output_files().map(|file| file.location.clone()))
200 }
201 }
202 }
203 output_files.sort();
204 let req = &plan.request;
205 let rows = run_database_export_jobs(
206 plan.jobs,
207 parse_parallelism_from_option_map(&req.with),
208 cancellation,
209 |job, token| {
210 let ctx = ctx.clone();
211 async move {
212 match job {
213 DatabaseExportJob::Metric(unit) => self
214 .export_logical_tables(
215 &unit,
216 &req.location,
217 &req.connection,
218 req.time_range.as_ref(),
219 LogicalTableExportLimits::default(),
220 &token,
221 ctx,
222 )
223 .await
224 .map(|summary| summary.rows),
225 DatabaseExportJob::Ordinary { table, output } => {
226 let info = table.table_info();
227 let copy = CopyTableRequest {
228 catalog_name: info.catalog_name.clone(),
229 schema_name: info.schema_name.clone(),
230 table_name: info.name.clone(),
231 location: output.location,
232 with: req.with.clone(),
233 connection: req.connection.clone(),
234 pattern: None,
235 direction: CopyDirection::Export,
236 timestamp_range: req.time_range,
237 limit: None,
238 };
239 self.copy_captured_table_to(table, copy, ctx).await
240 }
241 }
242 }
243 },
244 )
245 .await?;
246 Ok(DatabaseExportSummary { rows, output_files })
247 }
248}
249
250async fn run_database_export_jobs<J, F: Future<Output = Result<usize>>>(
251 jobs: impl IntoIterator<Item = J>,
252 parallelism: usize,
253 cancellation: &CancellationToken,
254 mut run: impl FnMut(J, CancellationToken) -> F,
255) -> Result<usize> {
256 let token = CancellationToken::new();
257 let mut jobs = jobs.into_iter();
258 let mut active = FuturesUnordered::new();
259 let mut first_error = None;
260 let mut rows = 0;
261 loop {
262 while first_error.is_none()
263 && !cancellation.is_cancelled()
264 && active.len() < parallelism.max(1)
265 {
266 let Some(job) = jobs.next() else { break };
267 active.push(run(job, token.clone()));
268 }
269 if first_error.is_none() && cancellation.is_cancelled() {
270 first_error = Some(error::DatabaseExportCancelledSnafu.build());
271 token.cancel();
272 }
273 if active.is_empty() {
274 break;
275 }
276 let result = tokio::select! {
277 biased;
278 _ = cancellation.cancelled(), if first_error.is_none() => {
279 first_error = Some(error::DatabaseExportCancelledSnafu.build());
280 token.cancel();
281 continue;
282 }
283 result = active.next() => result,
284 };
285 match result {
286 Some(Ok(count)) => rows += count,
287 Some(Err(err)) if first_error.is_none() => {
288 first_error = Some(err);
289 token.cancel();
290 }
291 Some(Err(err)) => common_telemetry::warn!(err; "Failed to drain database export job"),
292 None => break,
293 }
294 }
295 match first_error {
296 Some(err) => Err(err),
297 None => Ok(rows),
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use std::sync::Arc;
304 use std::sync::atomic::{AtomicUsize, Ordering};
305
306 use tokio::sync::{Semaphore, mpsc};
307
308 use super::*;
309
310 #[tokio::test]
311 async fn bounded_admission_and_drain() {
312 for cancel in [false, true] {
313 let cancellation = CancellationToken::new();
314 let start_error = Arc::new(Semaphore::new(0));
315 let finish_io = Arc::new(Semaphore::new(0));
316 let (started, mut receiver) = mpsc::unbounded_channel();
317 let finished = Arc::new(AtomicUsize::new(0));
318 let task = tokio::spawn({
319 let cancellation = cancellation.clone();
320 let start_error = start_error.clone();
321 let finish_io = finish_io.clone();
322 let finished = finished.clone();
323 async move {
324 run_database_export_jobs(0..4, 2, &cancellation, |job, token| {
325 let started = started.clone();
326 let start_error = start_error.clone();
327 let finish_io = finish_io.clone();
328 let finished = finished.clone();
329 async move {
330 started.send(job).unwrap();
331 if job == 0 {
332 if cancel {
333 token.cancelled().await;
334 } else {
335 start_error.acquire().await.unwrap().forget();
336 }
337 return InvalidDatabaseExportSnafu {
338 reason: "first error",
339 }
340 .fail();
341 }
342 token.cancelled().await;
344 started.send(10).unwrap();
345 finish_io.acquire().await.unwrap().forget();
346 finished.fetch_add(1, Ordering::SeqCst);
347 InvalidDatabaseExportSnafu {
348 reason: "drain error",
349 }
350 .fail()
351 }
352 })
353 .await
354 }
355 });
356 assert_eq!(receiver.recv().await, Some(0));
357 assert_eq!(receiver.recv().await, Some(1));
358 assert!(receiver.try_recv().is_err());
359 if cancel {
360 cancellation.cancel();
361 } else {
362 start_error.add_permits(1);
363 }
364 assert_eq!(receiver.recv().await, Some(10));
365 assert!(!task.is_finished());
366 finish_io.add_permits(1);
367 let err = task.await.unwrap().unwrap_err();
368 if cancel {
369 assert!(matches!(err, error::Error::DatabaseExportCancelled { .. }));
370 } else {
371 assert!(
372 matches!(err, error::Error::InvalidDatabaseExport { reason } if reason == "first error")
373 );
374 }
375 assert_eq!(finished.load(Ordering::SeqCst), 1);
376 assert_eq!(receiver.recv().await, None);
377 }
378 }
379
380 #[tokio::test]
381 async fn cancellation_before_admission_and_successful_refill() {
382 let token = CancellationToken::new();
383 token.cancel();
384 let result = run_database_export_jobs(0..4, 2, &token, |_, _| async {
385 panic!("cancelled job admitted")
386 })
387 .await;
388 assert!(matches!(
389 result,
390 Err(error::Error::DatabaseExportCancelled { .. })
391 ));
392 let started = AtomicUsize::new(0);
393 let result = run_database_export_jobs(0..7, 2, &CancellationToken::new(), |job, _| {
394 started.fetch_add(1, Ordering::SeqCst);
395 async move { Ok(job) }
396 })
397 .await
398 .unwrap();
399 assert_eq!(result, 21);
400 assert_eq!(started.load(Ordering::SeqCst), 7);
401 }
402}