use std::time::Duration;
use crate::{BatchHarvestSummary, SyncStats};
#[derive(Debug, Clone)]
pub enum HarvestEvent<'a> {
BatchStarted {
total_portals: usize,
},
PortalStarted {
portal_index: usize,
total_portals: usize,
portal_name: &'a str,
portal_url: &'a str,
},
ExistingDatasetsFound {
count: usize,
},
PortalDatasetsFound {
count: usize,
},
DatasetProcessed {
current: usize,
total: usize,
created: usize,
updated: usize,
unchanged: usize,
failed: usize,
skipped: usize,
},
PortalCompleted {
portal_index: usize,
total_portals: usize,
portal_name: &'a str,
stats: &'a SyncStats,
},
PortalFailed {
portal_index: usize,
total_portals: usize,
portal_name: &'a str,
error: &'a str,
},
BatchCompleted {
summary: &'a BatchHarvestSummary,
},
PortalCancelled {
portal_index: usize,
total_portals: usize,
portal_name: &'a str,
stats: &'a SyncStats,
},
BatchCancelled {
completed_portals: usize,
total_portals: usize,
},
StaleDetected {
count: usize,
},
CircuitBreakerOpen {
service: &'a str,
retry_after: Duration,
},
PreprocessingStarted {
total: usize,
},
PreprocessingCompleted {
changed: usize,
unchanged: usize,
failed: usize,
},
}
pub trait ProgressReporter: Send + Sync {
fn report(&self, event: HarvestEvent<'_>) {
let _ = event;
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct SilentReporter;
impl ProgressReporter for SilentReporter {}
#[derive(Debug, Default, Clone, Copy)]
pub struct TracingReporter;
impl ProgressReporter for TracingReporter {
fn report(&self, event: HarvestEvent<'_>) {
use tracing::{error, info};
match event {
HarvestEvent::BatchStarted { total_portals } => {
info!("Starting batch harvest of {} portal(s)", total_portals);
}
HarvestEvent::PortalStarted {
portal_index,
total_portals,
portal_name,
portal_url,
} => {
info!(
"[Portal {}/{}] {} ({})",
portal_index + 1,
total_portals,
portal_name,
portal_url
);
}
HarvestEvent::ExistingDatasetsFound { count } => {
info!("Found {} existing dataset(s) in database", count);
}
HarvestEvent::PortalDatasetsFound { count } => {
info!("Found {} dataset(s) on portal", count);
}
HarvestEvent::DatasetProcessed {
current,
total,
created,
updated,
unchanged,
failed,
skipped,
} => {
let pct = (current as f64 / total as f64 * 100.0) as u8;
if skipped > 0 {
info!(
"Progress: {}/{} ({}%) - {} new, {} updated, {} unchanged, {} failed, {} skipped",
current, total, pct, created, updated, unchanged, failed, skipped
);
} else {
info!(
"Progress: {}/{} ({}%) - {} new, {} updated, {} unchanged, {} failed",
current, total, pct, created, updated, unchanged, failed
);
}
}
HarvestEvent::PortalCompleted {
portal_index,
total_portals,
portal_name,
stats,
} => {
info!(
"[Portal {}/{}] {} completed: {} dataset(s) ({} created, {} updated, {} unchanged)",
portal_index + 1,
total_portals,
portal_name,
stats.total(),
stats.created,
stats.updated,
stats.unchanged
);
}
HarvestEvent::PortalFailed {
portal_index,
total_portals,
portal_name,
error,
} => {
error!(
"[Portal {}/{}] {} failed: {}",
portal_index + 1,
total_portals,
portal_name,
error
);
}
HarvestEvent::BatchCompleted { summary } => {
info!(
"Batch complete: {} portal(s), {} dataset(s) ({} successful, {} failed)",
summary.total_portals(),
summary.total_datasets(),
summary.successful_count(),
summary.failed_count()
);
}
HarvestEvent::PortalCancelled {
portal_index,
total_portals,
portal_name,
stats,
} => {
info!(
"[Portal {}/{}] {} cancelled: {} dataset(s) processed ({} created, {} updated, {} unchanged)",
portal_index + 1,
total_portals,
portal_name,
stats.total(),
stats.created,
stats.updated,
stats.unchanged
);
}
HarvestEvent::BatchCancelled {
completed_portals,
total_portals,
} => {
info!(
"Batch cancelled: {}/{} portal(s) completed before cancellation",
completed_portals, total_portals
);
}
HarvestEvent::StaleDetected { count } => {
use tracing::warn;
warn!(
"{} dataset(s) marked as stale (no longer found on portal)",
count
);
}
HarvestEvent::CircuitBreakerOpen {
service,
retry_after,
} => {
use tracing::warn;
warn!(
"Circuit breaker '{}' is open. Retry after {} seconds.",
service,
retry_after.as_secs()
);
}
HarvestEvent::PreprocessingStarted { total } => {
info!("Pre-processing {} dataset(s) (delta detection)...", total);
}
HarvestEvent::PreprocessingCompleted {
changed,
unchanged,
failed,
} => {
info!(
"Pre-processing complete: {} changed, {} unchanged, {} failed",
changed, unchanged, failed
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_silent_reporter_does_nothing() {
let reporter = SilentReporter;
reporter.report(HarvestEvent::BatchStarted { total_portals: 5 });
}
#[test]
fn test_tracing_reporter_handles_all_events() {
let reporter = TracingReporter;
reporter.report(HarvestEvent::BatchStarted { total_portals: 2 });
reporter.report(HarvestEvent::PortalStarted {
portal_index: 0,
total_portals: 2,
portal_name: "test",
portal_url: "https://example.com",
});
reporter.report(HarvestEvent::ExistingDatasetsFound { count: 10 });
reporter.report(HarvestEvent::PortalDatasetsFound { count: 20 });
reporter.report(HarvestEvent::DatasetProcessed {
current: 10,
total: 20,
created: 2,
updated: 3,
unchanged: 5,
failed: 0,
skipped: 0,
});
let stats = SyncStats {
unchanged: 5,
updated: 3,
created: 2,
failed: 0,
skipped: 0,
};
reporter.report(HarvestEvent::PortalCompleted {
portal_index: 0,
total_portals: 2,
portal_name: "test",
stats: &stats,
});
reporter.report(HarvestEvent::PortalFailed {
portal_index: 1,
total_portals: 2,
portal_name: "test2",
error: "connection failed",
});
let summary = BatchHarvestSummary::new();
reporter.report(HarvestEvent::BatchCompleted { summary: &summary });
reporter.report(HarvestEvent::PortalCancelled {
portal_index: 0,
total_portals: 2,
portal_name: "test",
stats: &stats,
});
reporter.report(HarvestEvent::BatchCancelled {
completed_portals: 1,
total_portals: 3,
});
reporter.report(HarvestEvent::StaleDetected { count: 5 });
reporter.report(HarvestEvent::CircuitBreakerOpen {
service: "gemini",
retry_after: Duration::from_secs(30),
});
reporter.report(HarvestEvent::PreprocessingStarted { total: 100 });
reporter.report(HarvestEvent::PreprocessingCompleted {
changed: 10,
unchanged: 85,
failed: 5,
});
}
#[test]
fn test_default_implementations() {
let silent = SilentReporter;
silent.report(HarvestEvent::BatchStarted { total_portals: 1 });
let tracing = TracingReporter;
tracing.report(HarvestEvent::BatchStarted { total_portals: 1 });
}
}