1use std::collections::BTreeSet;
16use std::path::{Path, PathBuf};
17use std::time::Instant;
18
19use async_trait::async_trait;
20use common_telemetry::{info, warn};
21use futures::StreamExt;
22use futures::stream::FuturesUnordered;
23
24use crate::data::export_v2::manifest::{ChunkMeta, ChunkStatus};
25use crate::data::import_v2::error::{
26 Error, ImportStateDdlIncompleteSnafu, ImportStateMismatchSnafu, Result,
27};
28use crate::data::import_v2::state::{
29 ImportState, ImportStateLockGuard, ImportTaskKey, ImportTaskStatus, canonical_schema_selection,
30 delete_import_state, load_import_state, save_import_state, try_acquire_import_state_lock,
31};
32use crate::data::path::data_dir_for_schema_chunk;
33#[cfg(test)]
34use crate::data::progress::NoopProgress;
35use crate::data::progress::{ProgressPhase, ProgressReporter};
36
37#[async_trait]
38pub(crate) trait ImportTaskExecutor {
39 async fn import_task(&self, task: &ImportTaskKey) -> Result<()>;
40}
41
42pub(crate) struct ImportResumeConfig {
43 pub(crate) snapshot_id: String,
44 pub(crate) target_addr: String,
45 pub(crate) catalog: String,
46 pub(crate) schemas: Vec<String>,
47 pub(crate) state_path: PathBuf,
48 pub(crate) tasks: Vec<ImportTaskKey>,
49 pub(crate) task_parallelism: usize,
51}
52
53pub(crate) struct ImportResumeSession {
54 config: ImportResumeConfig,
55 state: ImportState,
56 lock: ImportStateLockGuard,
57}
58
59impl ImportResumeSession {
60 pub(crate) fn should_skip_ddl(&self) -> bool {
61 self.state.ddl_completed
62 }
63
64 pub(crate) async fn mark_ddl_completed(&mut self) -> Result<()> {
68 self.state.mark_ddl_completed();
69 save_import_state(&self.config.state_path, &self.state).await
70 }
71}
72
73pub(crate) fn chunk_has_schema_files(chunk: &ChunkMeta, schema: &str) -> bool {
74 let prefix = data_dir_for_schema_chunk(schema, chunk.id);
75 chunk.files.iter().any(|path| {
76 let normalized = path.trim_start_matches('/');
77 normalized.starts_with(&prefix)
78 })
79}
80
81pub(crate) fn build_import_tasks(chunks: &[ChunkMeta], schemas: &[String]) -> Vec<ImportTaskKey> {
82 let mut tasks = Vec::new();
83 for chunk in chunks {
84 if chunk.status == ChunkStatus::Skipped {
85 continue;
86 }
87 for schema in schemas {
89 if chunk_has_schema_files(chunk, schema) {
90 tasks.push(ImportTaskKey::new(chunk.id, schema.clone()));
91 }
92 }
93 }
94 tasks
95}
96
97pub(crate) async fn prepare_import_resume(
98 config: ImportResumeConfig,
99) -> Result<ImportResumeSession> {
100 validate_config_tasks(&config)?;
104
105 let lock = try_acquire_import_state_lock(&config.state_path)?;
106 let state = match load_import_state(&config.state_path).await? {
107 Some(loaded) => {
108 validate_state_matches(&loaded, &config)?;
109 loaded
110 }
111 None => {
112 let fresh = ImportState::new(
118 &config.snapshot_id,
119 &config.target_addr,
120 &config.catalog,
121 &config.schemas,
122 config.tasks.clone(),
123 );
124 save_import_state(&config.state_path, &fresh).await?;
125 fresh
126 }
127 };
128
129 Ok(ImportResumeSession {
130 config,
131 state,
132 lock,
133 })
134}
135
136#[cfg(test)]
137pub(crate) async fn import_with_resume_session<E>(
138 session: ImportResumeSession,
139 executor: &E,
140) -> Result<()>
141where
142 E: ImportTaskExecutor + Sync,
143{
144 let progress = NoopProgress;
145 import_with_resume_session_with_progress(session, executor, &progress).await
146}
147
148pub(crate) async fn import_with_resume_session_with_progress<E>(
149 session: ImportResumeSession,
150 executor: &E,
151 progress: &dyn ProgressReporter,
152) -> Result<()>
153where
154 E: ImportTaskExecutor + Sync,
155{
156 let ImportResumeSession {
157 config,
158 mut state,
159 lock,
160 } = session;
161
162 if !state.ddl_completed {
167 return ImportStateDdlIncompleteSnafu {
168 path: config.state_path.display().to_string(),
169 }
170 .fail();
171 }
172
173 let completed = state
174 .tasks
175 .iter()
176 .filter(|task| task.status == ImportTaskStatus::Completed)
177 .count();
178 info!(
179 "Import resume state: {} completed, {} pending, path: {}",
180 completed,
181 state.tasks.len().saturating_sub(completed),
182 config.state_path.display()
183 );
184
185 let progress_phase = ProgressPhase::start(
188 progress,
189 "Import data tasks",
190 Some(config.tasks.len() as u64),
191 );
192
193 let import_start = Instant::now();
194 let result = if config.task_parallelism <= 1 {
195 import_tasks_serial(&config, &mut state, executor, progress).await
196 } else {
197 import_tasks_concurrent(
198 &config,
199 &mut state,
200 executor,
201 config.task_parallelism,
202 progress,
203 )
204 .await
205 };
206 progress_phase.finish();
207
208 result?;
210
211 delete_import_state(&config.state_path).await?;
212 info!("Data import finished in {:?}", import_start.elapsed());
213 drop(lock);
214 Ok(())
215}
216
217async fn import_tasks_serial<E>(
219 config: &ImportResumeConfig,
220 state: &mut ImportState,
221 executor: &E,
222 progress: &dyn ProgressReporter,
223) -> Result<()>
224where
225 E: ImportTaskExecutor + Sync,
226{
227 for (idx, task) in config.tasks.iter().enumerate() {
228 if state.task_status(task.chunk_id, &task.schema) == Some(ImportTaskStatus::Completed) {
229 info!(
230 "[{}/{}] Chunk {} schema {}: already completed, skipped",
231 idx + 1,
232 config.tasks.len(),
233 task.chunk_id,
234 task.schema
235 );
236 progress.inc(1);
237 continue;
238 }
239
240 info!(
241 "[{}/{}] Chunk {} schema {}: importing...",
242 idx + 1,
243 config.tasks.len(),
244 task.chunk_id,
245 task.schema
246 );
247 state.set_task_status(
248 task.chunk_id,
249 &task.schema,
250 ImportTaskStatus::InProgress,
251 None,
252 )?;
253 save_import_state(&config.state_path, state).await?;
254
255 let task_start = Instant::now();
256 let result = executor.import_task(task).await;
257
258 match result {
259 Ok(()) => {
260 update_status_and_save(config, state, task, ImportTaskStatus::Completed, None)
266 .await?;
267 info!(
268 "[{}/{}] Chunk {} schema {}: done in {:?}",
269 idx + 1,
270 config.tasks.len(),
271 task.chunk_id,
272 task.schema,
273 task_start.elapsed()
274 );
275 progress.inc(1);
276 }
277 Err(task_error) => {
278 persist_failed_best_effort(config, state, task, &task_error).await;
282 return Err(task_error);
283 }
284 }
285 }
286
287 Ok(())
288}
289
290async fn import_tasks_concurrent<E>(
302 config: &ImportResumeConfig,
303 state: &mut ImportState,
304 executor: &E,
305 task_parallelism: usize,
306 progress: &dyn ProgressReporter,
307) -> Result<()>
308where
309 E: ImportTaskExecutor + Sync,
310{
311 let mut pending = FuturesUnordered::new();
312 let mut next_idx = 0;
313 let mut first_error: Option<Error> = None;
314
315 loop {
316 let mut scheduled = false;
317
318 while first_error.is_none() && pending.len() < task_parallelism {
321 let Some(idx) = next_pending_task(config, state, &mut next_idx, progress) else {
322 break;
323 };
324
325 let task = &config.tasks[idx];
326 info!(
327 "[{}/{}] Chunk {} schema {}: importing...",
328 idx + 1,
329 config.tasks.len(),
330 task.chunk_id,
331 task.schema
332 );
333 state.set_task_status(
334 task.chunk_id,
335 &task.schema,
336 ImportTaskStatus::InProgress,
337 None,
338 )?;
339 scheduled = true;
340
341 pending.push(async move {
342 let result = executor.import_task(task).await;
343 (idx, result)
344 });
345 }
346
347 if scheduled {
348 save_import_state(&config.state_path, state).await?;
349 }
350
351 let Some((idx, task_result)) = pending.next().await else {
352 break;
353 };
354
355 let task = &config.tasks[idx];
356 match task_result {
357 Ok(()) => {
358 update_status_and_save(config, state, task, ImportTaskStatus::Completed, None)
362 .await?;
363 info!(
364 "[{}/{}] Chunk {} schema {}: done",
365 idx + 1,
366 config.tasks.len(),
367 task.chunk_id,
368 task.schema
369 );
370 progress.inc(1);
371 }
372 Err(task_error) => {
373 persist_failed_best_effort(config, state, task, &task_error).await;
376 if first_error.is_none() {
377 first_error = Some(task_error);
378 }
379 }
380 }
381 }
382
383 match first_error {
384 Some(err) => Err(err),
385 None => Ok(()),
386 }
387}
388
389fn next_pending_task(
394 config: &ImportResumeConfig,
395 state: &ImportState,
396 next_idx: &mut usize,
397 progress: &dyn ProgressReporter,
398) -> Option<usize> {
399 while *next_idx < config.tasks.len() {
400 let idx = *next_idx;
401 *next_idx += 1;
402 let task = &config.tasks[idx];
403 if state.task_status(task.chunk_id, &task.schema) == Some(ImportTaskStatus::Completed) {
404 info!(
405 "[{}/{}] Chunk {} schema {}: already completed, skipped",
406 idx + 1,
407 config.tasks.len(),
408 task.chunk_id,
409 task.schema
410 );
411 progress.inc(1);
412 continue;
413 }
414 return Some(idx);
415 }
416 None
417}
418
419async fn persist_failed_best_effort(
420 config: &ImportResumeConfig,
421 state: &mut ImportState,
422 task: &ImportTaskKey,
423 task_error: &Error,
424) {
425 if let Err(persist_error) = update_status_and_save(
426 config,
427 state,
428 task,
429 ImportTaskStatus::Failed,
430 Some(task_error.to_string()),
431 )
432 .await
433 {
434 warn!(
435 "Failed to persist Failed status for chunk {} schema {} after task error ({}); state file may be out of date: {}",
436 task.chunk_id, task.schema, task_error, persist_error
437 );
438 }
439}
440
441async fn update_status_and_save(
442 config: &ImportResumeConfig,
443 state: &mut ImportState,
444 task: &ImportTaskKey,
445 status: ImportTaskStatus,
446 error_message: Option<String>,
447) -> Result<()> {
448 state.set_task_status(task.chunk_id, &task.schema, status, error_message)?;
452 save_import_state(&config.state_path, state).await
453}
454
455fn validate_state_matches(state: &ImportState, config: &ImportResumeConfig) -> Result<()> {
456 if state.snapshot_id != config.snapshot_id {
457 return state_mismatch(
458 config,
459 format!(
460 "snapshot_id differs (state: {}, requested: {})",
461 state.snapshot_id, config.snapshot_id
462 ),
463 );
464 }
465 if state.target_addr != config.target_addr {
467 return state_mismatch(
468 config,
469 format!(
470 "target_addr differs (state: {}, requested: {})",
471 state.target_addr, config.target_addr
472 ),
473 );
474 }
475 if state.catalog != config.catalog {
476 return state_mismatch(
477 config,
478 format!(
479 "catalog differs (state: {}, requested: {})",
480 state.catalog, config.catalog
481 ),
482 );
483 }
484
485 let requested_schemas = canonical_schema_selection(&config.schemas);
486 if state.schemas != requested_schemas {
487 return state_mismatch(
488 config,
489 format!(
490 "schemas differ (state: {:?}, requested: {:?})",
491 state.schemas, requested_schemas
492 ),
493 );
494 }
495
496 if task_set_from_state(state, &config.state_path)? != task_set_from_config(config)? {
497 return state_mismatch(config, "task set differs".to_string());
498 }
499
500 Ok(())
501}
502
503fn state_mismatch(config: &ImportResumeConfig, reason: String) -> Result<()> {
504 ImportStateMismatchSnafu {
505 path: config.state_path.display().to_string(),
506 reason,
507 }
508 .fail()
509}
510
511fn task_set_from_state<'a>(
512 state: &'a ImportState,
513 state_path: &Path,
514) -> Result<BTreeSet<(u32, &'a str)>> {
515 let mut tasks = BTreeSet::new();
516 for task in &state.tasks {
517 if !tasks.insert((task.chunk_id, task.schema.as_str())) {
518 return ImportStateMismatchSnafu {
519 path: state_path.display().to_string(),
520 reason: format!(
521 "duplicate task key in state (chunk_id: {}, schema: {})",
522 task.chunk_id, task.schema
523 ),
524 }
525 .fail();
526 }
527 }
528 Ok(tasks)
529}
530
531fn task_set_from_config(config: &ImportResumeConfig) -> Result<BTreeSet<(u32, &str)>> {
532 let mut tasks = BTreeSet::new();
533 for task in &config.tasks {
534 if !tasks.insert((task.chunk_id, task.schema.as_str())) {
535 return ImportStateMismatchSnafu {
536 path: config.state_path.display().to_string(),
537 reason: format!(
538 "duplicate task key in request (chunk_id: {}, schema: {})",
539 task.chunk_id, task.schema
540 ),
541 }
542 .fail();
543 }
544 }
545 Ok(tasks)
546}
547
548fn validate_config_tasks(config: &ImportResumeConfig) -> Result<()> {
549 task_set_from_config(config).map(|_| ())
550}
551
552#[cfg(test)]
553mod tests {
554 use std::sync::atomic::{AtomicUsize, Ordering};
555 use std::sync::{Arc, Mutex};
556
557 use super::*;
558 use crate::data::export_v2::manifest::{ChunkMeta, TimeRange};
559 use crate::data::import_v2::error::TestTaskFailedSnafu;
560 use crate::data::progress::test_util::{ProgressEvent, RecordingProgress};
561
562 #[derive(Debug, Clone, Copy)]
563 enum FailureMode {
564 Fatal,
565 RetryableThenSuccess { failures: usize },
566 }
567
568 struct RecordingExecutor {
569 imported: Arc<Mutex<Vec<ImportTaskKey>>>,
570 fail_task: Option<ImportTaskKey>,
571 failure_mode: Option<FailureMode>,
572 attempts: Arc<AtomicUsize>,
573 }
574
575 #[async_trait]
576 impl ImportTaskExecutor for RecordingExecutor {
577 async fn import_task(&self, task: &ImportTaskKey) -> Result<()> {
578 let attempt = self.attempts.fetch_add(1, Ordering::SeqCst);
579 if self.fail_task.as_ref() == Some(task) {
580 match self.failure_mode {
581 Some(FailureMode::Fatal) => {
582 return TestTaskFailedSnafu {
583 message: "fatal failure".to_string(),
584 retryable: false,
585 }
586 .fail();
587 }
588 Some(FailureMode::RetryableThenSuccess { failures }) if attempt < failures => {
589 return TestTaskFailedSnafu {
590 message: "retryable failure".to_string(),
591 retryable: true,
592 }
593 .fail();
594 }
595 _ => {}
596 }
597 }
598 self.imported.lock().unwrap().push(task.clone());
599 Ok(())
600 }
601 }
602
603 fn recording_executor(imported: Arc<Mutex<Vec<ImportTaskKey>>>) -> RecordingExecutor {
604 RecordingExecutor {
605 imported,
606 fail_task: None,
607 failure_mode: None,
608 attempts: Arc::new(AtomicUsize::new(0)),
609 }
610 }
611
612 fn config(path: PathBuf, tasks: Vec<ImportTaskKey>) -> ImportResumeConfig {
613 config_with_parallelism(path, tasks, 1)
614 }
615
616 fn config_with_parallelism(
617 path: PathBuf,
618 tasks: Vec<ImportTaskKey>,
619 task_parallelism: usize,
620 ) -> ImportResumeConfig {
621 ImportResumeConfig {
622 snapshot_id: "snapshot-1".to_string(),
623 target_addr: "127.0.0.1:4000".to_string(),
624 catalog: "greptime".to_string(),
625 schemas: vec!["public".to_string(), "analytics".to_string()],
626 state_path: path,
627 tasks,
628 task_parallelism,
629 }
630 }
631
632 async fn run_import_with_resume<E>(config: ImportResumeConfig, executor: &E) -> Result<()>
633 where
634 E: ImportTaskExecutor + Sync,
635 {
636 let mut session = prepare_import_resume(config).await?;
640 if !session.should_skip_ddl() {
641 session.mark_ddl_completed().await?;
642 }
643 import_with_resume_session(session, executor).await
644 }
645
646 #[test]
647 fn test_build_import_tasks_skips_skipped_chunks_and_missing_schema_files() {
648 let mut completed = ChunkMeta::new(1, TimeRange::unbounded());
649 completed.status = ChunkStatus::Completed;
650 completed.files = vec!["data/public/1/file.parquet".to_string()];
651 let mut skipped = ChunkMeta::new(2, TimeRange::unbounded());
652 skipped.status = ChunkStatus::Skipped;
653 skipped.files = vec!["data/public/2/file.parquet".to_string()];
654
655 let tasks = build_import_tasks(
656 &[completed, skipped],
657 &["public".to_string(), "analytics".to_string()],
658 );
659
660 assert_eq!(tasks, vec![ImportTaskKey::new(1, "public")]);
661 }
662
663 #[tokio::test]
664 async fn test_import_with_resume_skips_completed_tasks() {
665 let dir = tempfile::tempdir().unwrap();
666 let path = dir.path().join("import_state.json");
667 let tasks = vec![
668 ImportTaskKey::new(1, "public"),
669 ImportTaskKey::new(2, "analytics"),
670 ];
671 let mut state = ImportState::new(
672 "snapshot-1",
673 "127.0.0.1:4000",
674 "greptime",
675 &["public".to_string(), "analytics".to_string()],
676 tasks.clone(),
677 );
678 state.mark_ddl_completed();
679 state
680 .set_task_status(1, "public", ImportTaskStatus::Completed, None)
681 .unwrap();
682 save_import_state(&path, &state).await.unwrap();
683
684 let imported = Arc::new(Mutex::new(Vec::new()));
685 let executor = recording_executor(imported.clone());
686
687 run_import_with_resume(config(path.clone(), tasks), &executor)
688 .await
689 .unwrap();
690
691 assert_eq!(
692 imported.lock().unwrap().clone(),
693 vec![ImportTaskKey::new(2, "analytics")]
694 );
695 assert!(load_import_state(&path).await.unwrap().is_none());
696 }
697
698 #[tokio::test]
699 async fn test_import_with_resume_persists_failed_task() {
700 let dir = tempfile::tempdir().unwrap();
701 let path = dir.path().join("import_state.json");
702 let failed_task = ImportTaskKey::new(1, "public");
703 let tasks = vec![failed_task.clone()];
704 let imported = Arc::new(Mutex::new(Vec::new()));
705 let executor = RecordingExecutor {
706 imported,
707 fail_task: Some(failed_task.clone()),
708 failure_mode: Some(FailureMode::Fatal),
709 attempts: Arc::new(AtomicUsize::new(0)),
710 };
711
712 let error = run_import_with_resume(config(path.clone(), tasks), &executor)
713 .await
714 .unwrap_err();
715 assert!(matches!(
716 error,
717 crate::data::import_v2::error::Error::TestTaskFailed {
718 retryable: false,
719 ..
720 }
721 ));
722
723 let state = load_import_state(&path).await.unwrap().unwrap();
724 assert_eq!(
725 state.task_status(failed_task.chunk_id, &failed_task.schema),
726 Some(ImportTaskStatus::Failed)
727 );
728 }
729
730 #[tokio::test]
731 async fn test_import_with_resume_rejects_mismatched_state_identity() {
732 let dir = tempfile::tempdir().unwrap();
733 let path = dir.path().join("import_state.json");
734 let tasks = vec![ImportTaskKey::new(1, "public")];
735 let state = ImportState::new(
736 "snapshot-1",
737 "127.0.0.1:4001",
738 "greptime",
739 &["public".to_string(), "analytics".to_string()],
740 tasks.clone(),
741 );
742 save_import_state(&path, &state).await.unwrap();
743
744 let imported = Arc::new(Mutex::new(Vec::new()));
745 let executor = recording_executor(imported);
746
747 let error = run_import_with_resume(config(path, tasks), &executor)
748 .await
749 .unwrap_err();
750
751 assert!(matches!(
752 error,
753 crate::data::import_v2::error::Error::ImportStateMismatch { .. }
754 ));
755 }
756
757 #[tokio::test]
758 async fn test_prepare_import_resume_reports_existing_state_before_ddl() {
759 let dir = tempfile::tempdir().unwrap();
760 let tasks = vec![ImportTaskKey::new(1, "public")];
761
762 let fresh_session =
763 prepare_import_resume(config(dir.path().join("fresh_state.json"), tasks.clone()))
764 .await
765 .unwrap();
766 assert!(!fresh_session.should_skip_ddl());
767 drop(fresh_session);
768
769 let existing_path = dir.path().join("existing_state.json");
770 let mut state = ImportState::new(
771 "snapshot-1",
772 "127.0.0.1:4000",
773 "greptime",
774 &["public".to_string(), "analytics".to_string()],
775 tasks.clone(),
776 );
777 state.mark_ddl_completed();
778 save_import_state(&existing_path, &state).await.unwrap();
779
780 let resume_session = prepare_import_resume(config(existing_path, tasks))
781 .await
782 .unwrap();
783 assert!(resume_session.should_skip_ddl());
784 }
785
786 #[tokio::test]
787 async fn test_import_with_resume_rejects_duplicate_state_tasks() {
788 let dir = tempfile::tempdir().unwrap();
789 let path = dir.path().join("import_state.json");
790 let tasks = vec![ImportTaskKey::new(1, "public")];
791 let mut state = ImportState::new(
792 "snapshot-1",
793 "127.0.0.1:4000",
794 "greptime",
795 &["public".to_string(), "analytics".to_string()],
796 tasks.clone(),
797 );
798 state.tasks.push(state.tasks[0].clone());
799 save_import_state(&path, &state).await.unwrap();
800
801 let imported = Arc::new(Mutex::new(Vec::new()));
802 let executor = recording_executor(imported);
803
804 let error = run_import_with_resume(config(path, tasks), &executor)
805 .await
806 .unwrap_err();
807
808 assert!(matches!(
809 error,
810 crate::data::import_v2::error::Error::ImportStateMismatch { .. }
811 ));
812 }
813
814 #[tokio::test]
815 async fn test_import_with_resume_rejects_data_import_when_ddl_incomplete() {
816 let dir = tempfile::tempdir().unwrap();
817 let path = dir.path().join("import_state.json");
818 let tasks = vec![ImportTaskKey::new(1, "public")];
819
820 let session = prepare_import_resume(config(path, tasks)).await.unwrap();
824 let imported = Arc::new(Mutex::new(Vec::new()));
825 let executor = recording_executor(imported.clone());
826
827 let error = import_with_resume_session(session, &executor)
828 .await
829 .unwrap_err();
830
831 assert!(matches!(
832 error,
833 crate::data::import_v2::error::Error::ImportStateDdlIncomplete { .. }
834 ));
835 assert!(imported.lock().unwrap().is_empty());
836 }
837
838 #[tokio::test]
839 async fn test_prepare_import_resume_rejects_duplicate_request_tasks_on_fresh_state() {
840 let dir = tempfile::tempdir().unwrap();
841 let path = dir.path().join("import_state.json");
842 let task = ImportTaskKey::new(1, "public");
843 let error =
847 match prepare_import_resume(config(path.clone(), vec![task.clone(), task])).await {
848 Ok(_) => panic!("duplicate request tasks should be rejected"),
849 Err(error) => error,
850 };
851
852 assert!(matches!(
853 error,
854 crate::data::import_v2::error::Error::ImportStateMismatch { .. }
855 ));
856 assert!(load_import_state(&path).await.unwrap().is_none());
857 }
858
859 #[tokio::test]
860 async fn test_import_with_resume_does_not_retry_retryable_task_error() {
861 let dir = tempfile::tempdir().unwrap();
862 let path = dir.path().join("import_state.json");
863 let failed_task = ImportTaskKey::new(1, "public");
864 let tasks = vec![failed_task.clone()];
865 let imported = Arc::new(Mutex::new(Vec::new()));
866 let attempts = Arc::new(AtomicUsize::new(0));
867 let executor = RecordingExecutor {
868 imported: imported.clone(),
869 fail_task: Some(failed_task.clone()),
870 failure_mode: Some(FailureMode::RetryableThenSuccess { failures: 1 }),
874 attempts: attempts.clone(),
875 };
876
877 let error = run_import_with_resume(config(path.clone(), tasks), &executor)
878 .await
879 .unwrap_err();
880
881 assert!(matches!(
882 error,
883 crate::data::import_v2::error::Error::TestTaskFailed {
884 retryable: true,
885 ..
886 }
887 ));
888 assert_eq!(attempts.load(Ordering::SeqCst), 1);
889 assert!(imported.lock().unwrap().is_empty());
890
891 let state = load_import_state(&path).await.unwrap().unwrap();
892 assert_eq!(
893 state.task_status(failed_task.chunk_id, &failed_task.schema),
894 Some(ImportTaskStatus::Failed)
895 );
896 }
897
898 #[tokio::test]
899 async fn test_import_progress_counts_skipped_and_completed_tasks() {
900 let dir = tempfile::tempdir().unwrap();
901 let path = dir.path().join("import_state.json");
902 let tasks = vec![
903 ImportTaskKey::new(1, "public"),
904 ImportTaskKey::new(2, "analytics"),
905 ];
906 let mut state = ImportState::new(
907 "snapshot-1",
908 "127.0.0.1:4000",
909 "greptime",
910 &["public".to_string(), "analytics".to_string()],
911 tasks.clone(),
912 );
913 state.mark_ddl_completed();
914 state
917 .set_task_status(1, "public", ImportTaskStatus::Completed, None)
918 .unwrap();
919 save_import_state(&path, &state).await.unwrap();
920
921 let imported = Arc::new(Mutex::new(Vec::new()));
922 let executor = recording_executor(imported.clone());
923 let progress = RecordingProgress::default();
924
925 let session = prepare_import_resume(config(path, tasks)).await.unwrap();
926 import_with_resume_session_with_progress(session, &executor, &progress)
927 .await
928 .unwrap();
929
930 assert_eq!(
932 imported.lock().unwrap().clone(),
933 vec![ImportTaskKey::new(2, "analytics")]
934 );
935
936 let events = progress.events();
937 assert_eq!(
938 events.first(),
939 Some(&ProgressEvent::StartPhase {
940 name: "Import data tasks".to_string(),
941 total: Some(2),
942 })
943 );
944 assert_eq!(events.last(), Some(&ProgressEvent::FinishPhase));
945 assert_eq!(progress.total_inc(), 2);
947 }
948
949 #[tokio::test]
950 async fn test_import_progress_stops_incrementing_on_task_failure() {
951 let dir = tempfile::tempdir().unwrap();
952 let path = dir.path().join("import_state.json");
953 let failed_task = ImportTaskKey::new(1, "public");
954 let tasks = vec![failed_task.clone(), ImportTaskKey::new(2, "analytics")];
955 let executor = RecordingExecutor {
956 imported: Arc::new(Mutex::new(Vec::new())),
957 fail_task: Some(failed_task.clone()),
958 failure_mode: Some(FailureMode::Fatal),
959 attempts: Arc::new(AtomicUsize::new(0)),
960 };
961
962 let mut state = ImportState::new(
963 "snapshot-1",
964 "127.0.0.1:4000",
965 "greptime",
966 &["public".to_string(), "analytics".to_string()],
967 tasks.clone(),
968 );
969 state.mark_ddl_completed();
970 save_import_state(&path, &state).await.unwrap();
971
972 let progress = RecordingProgress::default();
973 let session = prepare_import_resume(config(path, tasks)).await.unwrap();
974 import_with_resume_session_with_progress(session, &executor, &progress)
975 .await
976 .unwrap_err();
977
978 assert_eq!(progress.total_inc(), 0);
982 assert!(progress.events().contains(&ProgressEvent::FinishPhase));
983 }
984
985 struct ConcurrencyTrackingExecutor {
990 imported: Arc<Mutex<Vec<ImportTaskKey>>>,
991 in_flight: Arc<AtomicUsize>,
992 max_in_flight: Arc<AtomicUsize>,
993 }
994
995 #[async_trait]
996 impl ImportTaskExecutor for ConcurrencyTrackingExecutor {
997 async fn import_task(&self, task: &ImportTaskKey) -> Result<()> {
998 let current = self.in_flight.fetch_add(1, Ordering::SeqCst) + 1;
999 self.max_in_flight.fetch_max(current, Ordering::SeqCst);
1000 for _ in 0..8 {
1001 tokio::task::yield_now().await;
1002 }
1003 self.in_flight.fetch_sub(1, Ordering::SeqCst);
1004 self.imported.lock().unwrap().push(task.clone());
1005 Ok(())
1006 }
1007 }
1008
1009 fn ddl_completed_state(tasks: &[ImportTaskKey]) -> ImportState {
1010 let mut state = ImportState::new(
1011 "snapshot-1",
1012 "127.0.0.1:4000",
1013 "greptime",
1014 &["public".to_string(), "analytics".to_string()],
1015 tasks.to_vec(),
1016 );
1017 state.mark_ddl_completed();
1018 state
1019 }
1020
1021 #[tokio::test]
1022 async fn test_import_concurrent_caps_in_flight_tasks() {
1023 let dir = tempfile::tempdir().unwrap();
1024 let path = dir.path().join("import_state.json");
1025 let tasks: Vec<ImportTaskKey> = (0..8).map(|id| ImportTaskKey::new(id, "public")).collect();
1026 save_import_state(&path, &ddl_completed_state(&tasks))
1027 .await
1028 .unwrap();
1029
1030 let imported = Arc::new(Mutex::new(Vec::new()));
1031 let max_in_flight = Arc::new(AtomicUsize::new(0));
1032 let executor = ConcurrencyTrackingExecutor {
1033 imported: imported.clone(),
1034 in_flight: Arc::new(AtomicUsize::new(0)),
1035 max_in_flight: max_in_flight.clone(),
1036 };
1037
1038 let session =
1039 prepare_import_resume(config_with_parallelism(path.clone(), tasks.clone(), 3))
1040 .await
1041 .unwrap();
1042 import_with_resume_session(session, &executor)
1043 .await
1044 .unwrap();
1045
1046 let observed = max_in_flight.load(Ordering::SeqCst);
1048 assert!(
1049 observed <= 3,
1050 "observed {observed} in-flight, expected <= 3"
1051 );
1052 assert!(
1053 observed >= 2,
1054 "observed {observed} in-flight, expected concurrency"
1055 );
1056 assert_eq!(imported.lock().unwrap().len(), tasks.len());
1058 assert!(load_import_state(&path).await.unwrap().is_none());
1059 }
1060
1061 #[tokio::test]
1062 async fn test_import_concurrent_skips_completed_and_counts_progress_once() {
1063 let dir = tempfile::tempdir().unwrap();
1064 let path = dir.path().join("import_state.json");
1065 let tasks = vec![
1066 ImportTaskKey::new(1, "public"),
1067 ImportTaskKey::new(2, "analytics"),
1068 ImportTaskKey::new(3, "public"),
1069 ];
1070 let mut state = ddl_completed_state(&tasks);
1071 state
1072 .set_task_status(1, "public", ImportTaskStatus::Completed, None)
1073 .unwrap();
1074 save_import_state(&path, &state).await.unwrap();
1075
1076 let imported = Arc::new(Mutex::new(Vec::new()));
1077 let executor = recording_executor(imported.clone());
1078 let progress = RecordingProgress::default();
1079
1080 let session = prepare_import_resume(config_with_parallelism(path.clone(), tasks, 4))
1081 .await
1082 .unwrap();
1083 import_with_resume_session_with_progress(session, &executor, &progress)
1084 .await
1085 .unwrap();
1086
1087 let mut imported = imported.lock().unwrap().clone();
1089 imported.sort_by_key(|task| task.chunk_id);
1090 assert_eq!(
1091 imported,
1092 vec![
1093 ImportTaskKey::new(2, "analytics"),
1094 ImportTaskKey::new(3, "public"),
1095 ]
1096 );
1097 assert_eq!(progress.total_inc(), 3);
1099 assert!(load_import_state(&path).await.unwrap().is_none());
1100 }
1101
1102 #[tokio::test]
1103 async fn test_import_concurrent_persists_failed_and_returns_first_error() {
1104 let dir = tempfile::tempdir().unwrap();
1105 let path = dir.path().join("import_state.json");
1106 let failed_task = ImportTaskKey::new(1, "public");
1107 let tasks = vec![failed_task.clone(), ImportTaskKey::new(2, "analytics")];
1108 save_import_state(&path, &ddl_completed_state(&tasks))
1109 .await
1110 .unwrap();
1111
1112 let executor = RecordingExecutor {
1113 imported: Arc::new(Mutex::new(Vec::new())),
1114 fail_task: Some(failed_task.clone()),
1115 failure_mode: Some(FailureMode::Fatal),
1116 attempts: Arc::new(AtomicUsize::new(0)),
1117 };
1118
1119 let session = prepare_import_resume(config_with_parallelism(path.clone(), tasks, 4))
1120 .await
1121 .unwrap();
1122 let error = import_with_resume_session(session, &executor)
1123 .await
1124 .unwrap_err();
1125 assert!(matches!(
1126 error,
1127 crate::data::import_v2::error::Error::TestTaskFailed {
1128 retryable: false,
1129 ..
1130 }
1131 ));
1132
1133 let state = load_import_state(&path).await.unwrap().unwrap();
1135 assert_eq!(
1136 state.task_status(failed_task.chunk_id, &failed_task.schema),
1137 Some(ImportTaskStatus::Failed)
1138 );
1139 }
1140
1141 struct FailFastExecutor {
1142 imported: Arc<Mutex<Vec<ImportTaskKey>>>,
1143 fail_task: ImportTaskKey,
1144 }
1145
1146 #[async_trait]
1147 impl ImportTaskExecutor for FailFastExecutor {
1148 async fn import_task(&self, task: &ImportTaskKey) -> Result<()> {
1149 if task == &self.fail_task {
1150 return TestTaskFailedSnafu {
1151 message: "fatal failure".to_string(),
1152 retryable: false,
1153 }
1154 .fail();
1155 }
1156
1157 for _ in 0..8 {
1158 tokio::task::yield_now().await;
1159 }
1160 self.imported.lock().unwrap().push(task.clone());
1161 Ok(())
1162 }
1163 }
1164
1165 #[tokio::test]
1166 async fn test_import_concurrent_stops_scheduling_new_tasks_after_failure() {
1167 let dir = tempfile::tempdir().unwrap();
1168 let path = dir.path().join("import_state.json");
1169 let failed_task = ImportTaskKey::new(1, "public");
1170 let in_flight_task = ImportTaskKey::new(2, "analytics");
1171 let unscheduled_task_1 = ImportTaskKey::new(3, "public");
1172 let unscheduled_task_2 = ImportTaskKey::new(4, "analytics");
1173 let tasks = vec![
1174 failed_task.clone(),
1175 in_flight_task.clone(),
1176 unscheduled_task_1.clone(),
1177 unscheduled_task_2.clone(),
1178 ];
1179 save_import_state(&path, &ddl_completed_state(&tasks))
1180 .await
1181 .unwrap();
1182
1183 let imported = Arc::new(Mutex::new(Vec::new()));
1184 let executor = FailFastExecutor {
1185 imported: imported.clone(),
1186 fail_task: failed_task.clone(),
1187 };
1188
1189 let session = prepare_import_resume(config_with_parallelism(path.clone(), tasks, 2))
1190 .await
1191 .unwrap();
1192 import_with_resume_session(session, &executor)
1193 .await
1194 .unwrap_err();
1195
1196 assert_eq!(imported.lock().unwrap().clone(), vec![in_flight_task]);
1199 let state = load_import_state(&path).await.unwrap().unwrap();
1200 assert_eq!(
1201 state.task_status(failed_task.chunk_id, &failed_task.schema),
1202 Some(ImportTaskStatus::Failed)
1203 );
1204 assert_eq!(
1205 state.task_status(unscheduled_task_1.chunk_id, &unscheduled_task_1.schema),
1206 Some(ImportTaskStatus::Pending)
1207 );
1208 assert_eq!(
1209 state.task_status(unscheduled_task_2.chunk_id, &unscheduled_task_2.schema),
1210 Some(ImportTaskStatus::Pending)
1211 );
1212 }
1213}