1use std::time::Duration;
7
8use crate::{BatchHarvestSummary, SyncStats};
9
10#[derive(Debug, Clone)]
15pub enum HarvestEvent<'a> {
16 BatchStarted {
18 total_portals: usize,
20 },
21
22 PortalStarted {
24 portal_index: usize,
26 total_portals: usize,
28 portal_name: &'a str,
30 portal_url: &'a str,
32 },
33
34 ExistingDatasetsFound {
36 count: usize,
38 },
39
40 PortalDatasetsFound {
42 count: usize,
44 },
45
46 DatasetProcessed {
48 current: usize,
50 total: usize,
52 created: usize,
54 updated: usize,
56 unchanged: usize,
58 failed: usize,
60 skipped: usize,
62 },
63
64 PortalCompleted {
66 portal_index: usize,
68 total_portals: usize,
70 portal_name: &'a str,
72 stats: &'a SyncStats,
74 },
75
76 PortalFailed {
78 portal_index: usize,
80 total_portals: usize,
82 portal_name: &'a str,
84 error: &'a str,
86 },
87
88 BatchCompleted {
90 summary: &'a BatchHarvestSummary,
92 },
93
94 PortalCancelled {
96 portal_index: usize,
98 total_portals: usize,
100 portal_name: &'a str,
102 stats: &'a SyncStats,
104 },
105
106 BatchCancelled {
108 completed_portals: usize,
110 total_portals: usize,
112 },
113
114 CircuitBreakerOpen {
116 service: &'a str,
118 retry_after: Duration,
120 },
121}
122
123pub trait ProgressReporter: Send + Sync {
150 fn report(&self, event: HarvestEvent<'_>) {
154 let _ = event;
156 }
157}
158
159#[derive(Debug, Default, Clone, Copy)]
163pub struct SilentReporter;
164
165impl ProgressReporter for SilentReporter {}
166
167#[derive(Debug, Default, Clone, Copy)]
171pub struct TracingReporter;
172
173impl ProgressReporter for TracingReporter {
174 fn report(&self, event: HarvestEvent<'_>) {
175 use tracing::{error, info};
176
177 match event {
178 HarvestEvent::BatchStarted { total_portals } => {
179 info!("Starting batch harvest of {} portal(s)", total_portals);
180 }
181 HarvestEvent::PortalStarted {
182 portal_index,
183 total_portals,
184 portal_name,
185 portal_url,
186 } => {
187 info!(
188 "[Portal {}/{}] {} ({})",
189 portal_index + 1,
190 total_portals,
191 portal_name,
192 portal_url
193 );
194 }
195 HarvestEvent::ExistingDatasetsFound { count } => {
196 info!("Found {} existing dataset(s) in database", count);
197 }
198 HarvestEvent::PortalDatasetsFound { count } => {
199 info!("Found {} dataset(s) on portal", count);
200 }
201 HarvestEvent::DatasetProcessed {
202 current,
203 total,
204 created,
205 updated,
206 unchanged,
207 failed,
208 skipped,
209 } => {
210 let pct = (current as f64 / total as f64 * 100.0) as u8;
211 if skipped > 0 {
212 info!(
213 "Progress: {}/{} ({}%) - {} new, {} updated, {} unchanged, {} failed, {} skipped",
214 current, total, pct, created, updated, unchanged, failed, skipped
215 );
216 } else {
217 info!(
218 "Progress: {}/{} ({}%) - {} new, {} updated, {} unchanged, {} failed",
219 current, total, pct, created, updated, unchanged, failed
220 );
221 }
222 }
223 HarvestEvent::PortalCompleted {
224 portal_index,
225 total_portals,
226 portal_name,
227 stats,
228 } => {
229 info!(
230 "[Portal {}/{}] {} completed: {} dataset(s) ({} created, {} updated, {} unchanged)",
231 portal_index + 1,
232 total_portals,
233 portal_name,
234 stats.total(),
235 stats.created,
236 stats.updated,
237 stats.unchanged
238 );
239 }
240 HarvestEvent::PortalFailed {
241 portal_index,
242 total_portals,
243 portal_name,
244 error,
245 } => {
246 error!(
247 "[Portal {}/{}] {} failed: {}",
248 portal_index + 1,
249 total_portals,
250 portal_name,
251 error
252 );
253 }
254 HarvestEvent::BatchCompleted { summary } => {
255 info!(
256 "Batch complete: {} portal(s), {} dataset(s) ({} successful, {} failed)",
257 summary.total_portals(),
258 summary.total_datasets(),
259 summary.successful_count(),
260 summary.failed_count()
261 );
262 }
263 HarvestEvent::PortalCancelled {
264 portal_index,
265 total_portals,
266 portal_name,
267 stats,
268 } => {
269 info!(
270 "[Portal {}/{}] {} cancelled: {} dataset(s) processed ({} created, {} updated, {} unchanged)",
271 portal_index + 1,
272 total_portals,
273 portal_name,
274 stats.total(),
275 stats.created,
276 stats.updated,
277 stats.unchanged
278 );
279 }
280 HarvestEvent::BatchCancelled {
281 completed_portals,
282 total_portals,
283 } => {
284 info!(
285 "Batch cancelled: {}/{} portal(s) completed before cancellation",
286 completed_portals, total_portals
287 );
288 }
289 HarvestEvent::CircuitBreakerOpen {
290 service,
291 retry_after,
292 } => {
293 use tracing::warn;
294 warn!(
295 "Circuit breaker '{}' is open. Retry after {} seconds.",
296 service,
297 retry_after.as_secs()
298 );
299 }
300 }
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn test_silent_reporter_does_nothing() {
310 let reporter = SilentReporter;
311 reporter.report(HarvestEvent::BatchStarted { total_portals: 5 });
313 }
314
315 #[test]
316 fn test_tracing_reporter_handles_all_events() {
317 let reporter = TracingReporter;
318
319 reporter.report(HarvestEvent::BatchStarted { total_portals: 2 });
321 reporter.report(HarvestEvent::PortalStarted {
322 portal_index: 0,
323 total_portals: 2,
324 portal_name: "test",
325 portal_url: "https://example.com",
326 });
327 reporter.report(HarvestEvent::ExistingDatasetsFound { count: 10 });
328 reporter.report(HarvestEvent::PortalDatasetsFound { count: 20 });
329 reporter.report(HarvestEvent::DatasetProcessed {
330 current: 10,
331 total: 20,
332 created: 2,
333 updated: 3,
334 unchanged: 5,
335 failed: 0,
336 skipped: 0,
337 });
338
339 let stats = SyncStats {
340 unchanged: 5,
341 updated: 3,
342 created: 2,
343 failed: 0,
344 skipped: 0,
345 };
346 reporter.report(HarvestEvent::PortalCompleted {
347 portal_index: 0,
348 total_portals: 2,
349 portal_name: "test",
350 stats: &stats,
351 });
352 reporter.report(HarvestEvent::PortalFailed {
353 portal_index: 1,
354 total_portals: 2,
355 portal_name: "test2",
356 error: "connection failed",
357 });
358
359 let summary = BatchHarvestSummary::new();
360 reporter.report(HarvestEvent::BatchCompleted { summary: &summary });
361
362 reporter.report(HarvestEvent::PortalCancelled {
364 portal_index: 0,
365 total_portals: 2,
366 portal_name: "test",
367 stats: &stats,
368 });
369 reporter.report(HarvestEvent::BatchCancelled {
370 completed_portals: 1,
371 total_portals: 3,
372 });
373
374 reporter.report(HarvestEvent::CircuitBreakerOpen {
376 service: "gemini",
377 retry_after: Duration::from_secs(30),
378 });
379 }
380
381 #[test]
382 fn test_default_implementations() {
383 let silent = SilentReporter;
384 silent.report(HarvestEvent::BatchStarted { total_portals: 1 });
385
386 let tracing = TracingReporter;
387 tracing.report(HarvestEvent::BatchStarted { total_portals: 1 });
388 }
389}