1use crate::{
2 application::{dto::QueryEventsRequest, services::projection::Projection},
3 domain::entities::Event,
4 error::{AllSourceError, Result},
5 store::EventStore,
6};
7use chrono::{DateTime, Utc};
8use parking_lot::RwLock;
9use serde::{Deserialize, Serialize};
10use std::sync::{
11 Arc,
12 atomic::{AtomicBool, AtomicU64, Ordering},
13};
14use uuid::Uuid;
15
16#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
18#[serde(rename_all = "lowercase")]
19pub enum ReplayStatus {
20 Pending,
22 Running,
24 Completed,
26 Failed,
28 Cancelled,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ReplayConfig {
35 pub batch_size: usize,
37
38 pub parallel: bool,
40
41 pub workers: usize,
43
44 pub emit_progress: bool,
46
47 pub progress_interval: usize,
49}
50
51impl Default for ReplayConfig {
52 fn default() -> Self {
53 Self {
54 batch_size: 1000,
55 parallel: false,
56 workers: 4,
57 emit_progress: true,
58 progress_interval: 1000,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct StartReplayRequest {
66 pub projection_name: Option<String>,
68
69 pub from_timestamp: Option<DateTime<Utc>>,
71
72 pub to_timestamp: Option<DateTime<Utc>>,
74
75 pub entity_id: Option<String>,
77
78 pub event_type: Option<String>,
80
81 pub config: Option<ReplayConfig>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct StartReplayResponse {
88 pub replay_id: Uuid,
89 pub status: ReplayStatus,
90 pub started_at: DateTime<Utc>,
91 pub total_events: usize,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ReplayProgress {
97 pub replay_id: Uuid,
98 pub status: ReplayStatus,
99 pub started_at: DateTime<Utc>,
100 pub updated_at: DateTime<Utc>,
101 pub completed_at: Option<DateTime<Utc>>,
102 pub total_events: usize,
103 pub processed_events: usize,
104 pub failed_events: usize,
105 pub progress_percentage: f64,
106 pub events_per_second: f64,
107 pub error_message: Option<String>,
108}
109
110pub struct ReplayManager {
112 replays: Arc<RwLock<Vec<ReplayState>>>,
114}
115
116struct ReplayState {
118 id: Uuid,
119 projection_name: Option<String>,
120 status: Arc<RwLock<ReplayStatus>>,
121 started_at: DateTime<Utc>,
122 completed_at: Arc<RwLock<Option<DateTime<Utc>>>>,
123 total_events: usize,
124 processed_events: Arc<AtomicU64>,
125 failed_events: Arc<AtomicU64>,
126 error_message: Arc<RwLock<Option<String>>>,
127 cancelled: Arc<AtomicBool>,
128}
129
130impl ReplayManager {
131 pub fn new() -> Self {
132 Self {
133 replays: Arc::new(RwLock::new(Vec::new())),
134 }
135 }
136
137 pub fn start_replay(
139 &self,
140 store: Arc<EventStore>,
141 request: StartReplayRequest,
142 ) -> Result<StartReplayResponse> {
143 let replay_id = Uuid::new_v4();
144 let started_at = Utc::now();
145 let config = request.config.unwrap_or_default();
146
147 let query = QueryEventsRequest {
149 entity_id: request.entity_id.clone(),
150 event_type: request.event_type.clone(),
151 tenant_id: None,
152 as_of: request.to_timestamp,
153 since: request.from_timestamp,
154 until: request.to_timestamp,
155 limit: None,
156 event_type_prefix: None,
157 exclude_event_type_prefix: None,
158 payload_filter: None,
159 };
160
161 let events = store.query(&query)?;
162 let total_events = events.len();
163
164 tracing::info!(
165 "🔄 Starting replay {} for {} events{}",
166 replay_id,
167 total_events,
168 request
169 .projection_name
170 .as_ref()
171 .map(|n| format!(" (projection: {n})"))
172 .unwrap_or_default()
173 );
174
175 let state = ReplayState {
177 id: replay_id,
178 projection_name: request.projection_name.clone(),
179 status: Arc::new(RwLock::new(ReplayStatus::Running)),
180 started_at,
181 completed_at: Arc::new(RwLock::new(None)),
182 total_events,
183 processed_events: Arc::new(AtomicU64::new(0)),
184 failed_events: Arc::new(AtomicU64::new(0)),
185 error_message: Arc::new(RwLock::new(None)),
186 cancelled: Arc::new(AtomicBool::new(false)),
187 };
188
189 self.replays.write().push(state);
191
192 let replays = Arc::clone(&self.replays);
194 let replay_idx = replays.read().len() - 1;
195
196 tokio::spawn(async move {
198 let result = Self::run_replay(
199 store,
200 events,
201 request.projection_name,
202 config,
203 replays.clone(),
204 replay_idx,
205 )
206 .await;
207
208 let mut replays_lock = replays.write();
210 if let Some(state) = replays_lock.get_mut(replay_idx) {
211 *state.completed_at.write() = Some(Utc::now());
212
213 match result {
214 Ok(()) => {
215 if state.cancelled.load(Ordering::Relaxed) {
216 *state.status.write() = ReplayStatus::Cancelled;
217 tracing::info!("🛑 Replay {} cancelled", state.id);
218 } else {
219 *state.status.write() = ReplayStatus::Completed;
220 tracing::info!("✅ Replay {} completed successfully", state.id);
221 }
222 }
223 Err(e) => {
224 *state.status.write() = ReplayStatus::Failed;
225 *state.error_message.write() = Some(e.to_string());
226 tracing::error!("❌ Replay {} failed: {}", state.id, e);
227 }
228 }
229 }
230 });
231
232 Ok(StartReplayResponse {
233 replay_id,
234 status: ReplayStatus::Running,
235 started_at,
236 total_events,
237 })
238 }
239
240 #[allow(unknown_lints, clippy::unused_async_trait_impl)]
247 async fn run_replay(
248 store: Arc<EventStore>,
249 events: Vec<Event>,
250 projection_name: Option<String>,
251 config: ReplayConfig,
252 replays: Arc<RwLock<Vec<ReplayState>>>,
253 replay_idx: usize,
254 ) -> Result<()> {
255 let total = events.len();
256 let projections = store.projections.read();
257
258 let target_projections: Vec<(String, Arc<dyn Projection>)> =
260 if let Some(name) = projection_name {
261 if let Some(proj) = projections.get_projection(&name) {
262 vec![(name, proj)]
263 } else {
264 return Err(AllSourceError::ValidationError(format!(
265 "Projection not found: {name}"
266 )));
267 }
268 } else {
269 projections.list_projections()
270 };
271
272 drop(projections); for (batch_idx, chunk) in events.chunks(config.batch_size).enumerate() {
276 {
278 let replays_lock = replays.read();
279 if let Some(state) = replays_lock.get(replay_idx)
280 && state.cancelled.load(Ordering::Relaxed)
281 {
282 return Ok(());
283 }
284 }
285
286 for event in chunk {
288 for (proj_name, projection) in &target_projections {
290 if let Err(e) = projection.process(event) {
291 tracing::warn!(
292 "Failed to process event {} in projection {}: {}",
293 event.id,
294 proj_name,
295 e
296 );
297
298 let replays_lock = replays.read();
300 if let Some(state) = replays_lock.get(replay_idx) {
301 state.failed_events.fetch_add(1, Ordering::Relaxed);
302 }
303 }
304 }
305
306 let replays_lock = replays.read();
308 if let Some(state) = replays_lock.get(replay_idx) {
309 let processed = state.processed_events.fetch_add(1, Ordering::Relaxed) + 1;
310
311 if config.emit_progress && processed % config.progress_interval as u64 == 0 {
313 let progress = (processed as f64 / total as f64) * 100.0;
314 tracing::debug!(
315 "Replay progress: {}/{} ({:.1}%)",
316 processed,
317 total,
318 progress
319 );
320 }
321 }
322 }
323 }
324
325 Ok(())
326 }
327
328 pub fn get_progress(&self, replay_id: Uuid) -> Result<ReplayProgress> {
330 let replays = self.replays.read();
331
332 let state = replays.iter().find(|r| r.id == replay_id).ok_or_else(|| {
333 AllSourceError::ValidationError(format!("Replay not found: {replay_id}"))
334 })?;
335
336 let processed = state.processed_events.load(Ordering::Relaxed);
337 let failed = state.failed_events.load(Ordering::Relaxed);
338 let total_events = state.total_events;
339 let started_at = state.started_at;
340 let status = *state.status.read();
341 let completed_at = *state.completed_at.read();
342 let error_message = state.error_message.read().clone();
343
344 drop(replays); let progress_percentage = if total_events > 0 {
347 (processed as f64 / total_events as f64) * 100.0
348 } else {
349 0.0
350 };
351
352 let updated_at = Utc::now();
353 let elapsed_seconds = (updated_at - started_at).num_seconds().max(1) as f64;
354 let events_per_second = processed as f64 / elapsed_seconds;
355
356 Ok(ReplayProgress {
357 replay_id,
358 status,
359 started_at,
360 updated_at,
361 completed_at,
362 total_events,
363 processed_events: processed as usize,
364 failed_events: failed as usize,
365 progress_percentage,
366 events_per_second,
367 error_message,
368 })
369 }
370
371 pub fn cancel_replay(&self, replay_id: Uuid) -> Result<()> {
373 let replays = self.replays.read();
374
375 let state = replays.iter().find(|r| r.id == replay_id).ok_or_else(|| {
376 AllSourceError::ValidationError(format!("Replay not found: {replay_id}"))
377 })?;
378
379 let status = *state.status.read();
380 if status != ReplayStatus::Running {
381 return Err(AllSourceError::ValidationError(format!(
382 "Cannot cancel replay in status: {status:?}"
383 )));
384 }
385
386 state.cancelled.store(true, Ordering::Relaxed);
387 tracing::info!("🛑 Cancelling replay {}", replay_id);
388
389 Ok(())
390 }
391
392 pub fn list_replays(&self) -> Vec<ReplayProgress> {
394 let replays = self.replays.read();
395
396 replays
397 .iter()
398 .map(|state| {
399 let processed = state.processed_events.load(Ordering::Relaxed);
400 let failed = state.failed_events.load(Ordering::Relaxed);
401 let progress_percentage = if state.total_events > 0 {
402 (processed as f64 / state.total_events as f64) * 100.0
403 } else {
404 0.0
405 };
406
407 let updated_at = Utc::now();
408 let elapsed_seconds = (updated_at - state.started_at).num_seconds().max(1) as f64;
409 let events_per_second = processed as f64 / elapsed_seconds;
410
411 ReplayProgress {
412 replay_id: state.id,
413 status: *state.status.read(),
414 started_at: state.started_at,
415 updated_at,
416 completed_at: *state.completed_at.read(),
417 total_events: state.total_events,
418 processed_events: processed as usize,
419 failed_events: failed as usize,
420 progress_percentage,
421 events_per_second,
422 error_message: state.error_message.read().clone(),
423 }
424 })
425 .collect()
426 }
427
428 pub fn delete_replay(&self, replay_id: Uuid) -> Result<bool> {
430 let mut replays = self.replays.write();
431
432 let idx = replays
433 .iter()
434 .position(|r| r.id == replay_id)
435 .ok_or_else(|| {
436 AllSourceError::ValidationError(format!("Replay not found: {replay_id}"))
437 })?;
438
439 let status = *replays[idx].status.read();
440 if status == ReplayStatus::Running {
441 return Err(AllSourceError::ValidationError(
442 "Cannot delete a running replay. Cancel it first.".to_string(),
443 ));
444 }
445
446 replays.remove(idx);
447 tracing::info!("🗑️ Deleted replay {}", replay_id);
448
449 Ok(true)
450 }
451}
452
453impl Default for ReplayManager {
454 fn default() -> Self {
455 Self::new()
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use crate::domain::entities::Event;
463 use serde_json::json;
464
465 #[tokio::test]
466 async fn test_replay_manager_creation() {
467 let manager = ReplayManager::new();
468 let replays = manager.list_replays();
469 assert_eq!(replays.len(), 0);
470 }
471
472 #[tokio::test]
473 async fn test_replay_progress_tracking() {
474 let manager = ReplayManager::new();
475 let store = Arc::new(EventStore::new());
476
477 for i in 0..10 {
479 let event = Event::from_strings(
480 "test.event".to_string(),
481 "test-entity".to_string(),
482 "default".to_string(),
483 json!({"value": i}),
484 None,
485 )
486 .unwrap();
487 store.ingest(&event).unwrap();
488 }
489
490 let request = StartReplayRequest {
492 projection_name: None,
493 from_timestamp: None,
494 to_timestamp: None,
495 entity_id: None,
496 event_type: None,
497 config: Some(ReplayConfig {
498 batch_size: 5,
499 parallel: false,
500 workers: 1,
501 emit_progress: true,
502 progress_interval: 5,
503 }),
504 };
505
506 let response = manager.start_replay(store, request).unwrap();
507 assert_eq!(response.status, ReplayStatus::Running);
508 assert_eq!(response.total_events, 10);
509
510 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
512
513 let progress = manager.get_progress(response.replay_id).unwrap();
515 assert!(progress.processed_events <= 10);
516 }
517}