1use super::backends::{SessionBackend, SessionError};
26use super::cleanup::CleanupableBackend;
27use async_trait::async_trait;
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct MigrationResult {
50 pub total: usize,
52 pub migrated: usize,
54 pub failed: usize,
56 pub errors: Vec<String>,
58}
59
60impl MigrationResult {
61 pub fn new() -> Self {
73 Self {
74 total: 0,
75 migrated: 0,
76 failed: 0,
77 errors: Vec::new(),
78 }
79 }
80
81 pub fn is_successful(&self) -> bool {
97 self.failed == 0
98 }
99
100 pub fn success_rate(&self) -> f64 {
115 if self.total == 0 {
116 return 0.0;
117 }
118 (self.migrated as f64 / self.total as f64) * 100.0
119 }
120}
121
122impl Default for MigrationResult {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128#[derive(Debug, Clone)]
142pub struct MigrationConfig {
143 pub batch_size: usize,
145 pub skip_existing: bool,
147 pub verify_migration: bool,
149}
150
151impl Default for MigrationConfig {
152 fn default() -> Self {
165 Self {
166 batch_size: 1000,
167 skip_existing: false,
168 verify_migration: false,
169 }
170 }
171}
172
173#[async_trait]
175pub trait Migrator {
176 async fn migrate(&self) -> Result<MigrationResult, SessionError>;
178
179 async fn dry_run(&self) -> Result<usize, SessionError>;
181}
182
183pub struct SessionMigrator<S: SessionBackend, T: SessionBackend> {
207 source: S,
208 target: T,
209 config: MigrationConfig,
210}
211
212impl<S: SessionBackend, T: SessionBackend> SessionMigrator<S, T> {
213 pub fn new(source: S, target: T) -> Self {
226 Self {
227 source,
228 target,
229 config: MigrationConfig::default(),
230 }
231 }
232
233 pub fn with_config(source: S, target: T, config: MigrationConfig) -> Self {
251 Self {
252 source,
253 target,
254 config,
255 }
256 }
257}
258
259#[async_trait]
260impl<S, T> Migrator for SessionMigrator<S, T>
261where
262 S: SessionBackend + CleanupableBackend,
263 T: SessionBackend,
264{
265 async fn migrate(&self) -> Result<MigrationResult, SessionError> {
266 let mut result = MigrationResult::new();
267
268 let all_keys = self.source.get_all_keys().await?;
270 result.total = all_keys.len();
271
272 for chunk in all_keys.chunks(self.config.batch_size) {
274 for key in chunk {
275 if self.config.skip_existing && self.target.exists(key).await? {
277 continue;
278 }
279
280 match self
282 .source
283 .load::<HashMap<String, serde_json::Value>>(key)
284 .await
285 {
286 Ok(Some(data)) => {
287 match self.target.save(key, &data, None).await {
289 Ok(_) => {
290 if self.config.verify_migration {
292 match self
293 .target
294 .load::<HashMap<String, serde_json::Value>>(key)
295 .await
296 {
297 Ok(Some(_)) => result.migrated += 1,
298 Ok(None) => {
299 result.failed += 1;
300 result.errors.push(format!(
301 "Verification failed for key: {}",
302 key
303 ));
304 }
305 Err(e) => {
306 result.failed += 1;
307 result.errors.push(format!(
308 "Verification error for key {}: {}",
309 key, e
310 ));
311 }
312 }
313 } else {
314 result.migrated += 1;
315 }
316 }
317 Err(e) => {
318 result.failed += 1;
319 result
320 .errors
321 .push(format!("Failed to save key {}: {}", key, e));
322 }
323 }
324 }
325 Ok(None) => {
326 }
328 Err(e) => {
329 result.failed += 1;
330 result
331 .errors
332 .push(format!("Failed to load key {}: {}", key, e));
333 }
334 }
335 }
336 }
337
338 Ok(result)
339 }
340
341 async fn dry_run(&self) -> Result<usize, SessionError> {
342 let all_keys = self.source.get_all_keys().await?;
343 Ok(all_keys.len())
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::sessions::backends::InMemorySessionBackend;
351 use rstest::rstest;
352
353 #[rstest]
354 #[test]
355 fn test_migration_result_new() {
356 let result = MigrationResult::new();
357 assert_eq!(result.total, 0);
358 assert_eq!(result.migrated, 0);
359 assert_eq!(result.failed, 0);
360 assert!(result.errors.is_empty());
361 }
362
363 #[rstest]
364 #[test]
365 fn test_migration_result_is_successful() {
366 let mut result = MigrationResult::new();
367 result.total = 10;
368 result.migrated = 10;
369 assert!(result.is_successful());
370
371 result.failed = 1;
372 assert!(!result.is_successful());
373 }
374
375 #[rstest]
376 #[test]
377 fn test_migration_result_success_rate() {
378 let mut result = MigrationResult::new();
379 result.total = 100;
380 result.migrated = 95;
381 result.failed = 5;
382
383 assert_eq!(result.success_rate(), 95.0);
384 }
385
386 #[rstest]
387 #[test]
388 fn test_migration_result_success_rate_zero_total() {
389 let result = MigrationResult::new();
390 assert_eq!(result.success_rate(), 0.0);
391 }
392
393 #[rstest]
394 #[test]
395 fn test_migration_config_default() {
396 let config = MigrationConfig::default();
397 assert_eq!(config.batch_size, 1000);
398 assert!(!config.skip_existing);
399 assert!(!config.verify_migration);
400 }
401
402 #[rstest]
403 #[tokio::test]
404 async fn test_migrate_empty_source() {
405 let source = InMemorySessionBackend::new();
407 let target = InMemorySessionBackend::new();
408 let migrator = SessionMigrator::new(source, target);
409
410 let result = migrator.migrate().await.unwrap();
412
413 assert_eq!(result.total, 0);
415 assert_eq!(result.migrated, 0);
416 assert_eq!(result.failed, 0);
417 assert!(result.errors.is_empty());
418 assert!(result.is_successful());
419 }
420
421 #[rstest]
422 #[tokio::test]
423 async fn test_migrate_multiple_sessions() {
424 let source = InMemorySessionBackend::new();
426 let target = InMemorySessionBackend::new();
427
428 let data1: HashMap<String, serde_json::Value> =
429 [("user".into(), serde_json::json!("alice"))].into();
430 let data2: HashMap<String, serde_json::Value> =
431 [("user".into(), serde_json::json!("bob"))].into();
432 let data3: HashMap<String, serde_json::Value> =
433 [("user".into(), serde_json::json!("carol"))].into();
434
435 source.save("key1", &data1, None).await.unwrap();
436 source.save("key2", &data2, None).await.unwrap();
437 source.save("key3", &data3, None).await.unwrap();
438
439 let migrator = SessionMigrator::new(source, target.clone());
440
441 let result = migrator.migrate().await.unwrap();
443
444 assert_eq!(result.total, 3);
446 assert_eq!(result.migrated, 3);
447 assert_eq!(result.failed, 0);
448 assert!(target.exists("key1").await.unwrap());
449 assert!(target.exists("key2").await.unwrap());
450 assert!(target.exists("key3").await.unwrap());
451 }
452
453 #[rstest]
454 #[tokio::test]
455 async fn test_migrate_skip_existing_preserves_target() {
456 let source = InMemorySessionBackend::new();
458 let target = InMemorySessionBackend::new();
459
460 let source_data: HashMap<String, serde_json::Value> =
461 [("origin".into(), serde_json::json!("source_data"))].into();
462 let target_data: HashMap<String, serde_json::Value> =
463 [("origin".into(), serde_json::json!("target_data"))].into();
464
465 source.save("key1", &source_data, None).await.unwrap();
466 target.save("key1", &target_data, None).await.unwrap();
467
468 let config = MigrationConfig {
469 batch_size: 1000,
470 skip_existing: true,
471 verify_migration: false,
472 };
473 let migrator = SessionMigrator::with_config(source, target.clone(), config);
474
475 let result = migrator.migrate().await.unwrap();
477
478 assert_eq!(result.total, 1);
480 assert_eq!(result.migrated, 0);
482 let loaded: Option<HashMap<String, serde_json::Value>> = target.load("key1").await.unwrap();
483 assert_eq!(loaded.unwrap()["origin"], serde_json::json!("target_data"));
484 }
485
486 #[rstest]
487 #[tokio::test]
488 async fn test_migrate_with_verification_reads_back() {
489 let source = InMemorySessionBackend::new();
491 let target = InMemorySessionBackend::new();
492
493 let data1: HashMap<String, serde_json::Value> =
494 [("val".into(), serde_json::json!(1))].into();
495 let data2: HashMap<String, serde_json::Value> =
496 [("val".into(), serde_json::json!(2))].into();
497
498 source.save("sess_a", &data1, None).await.unwrap();
499 source.save("sess_b", &data2, None).await.unwrap();
500
501 let config = MigrationConfig {
502 batch_size: 1000,
503 skip_existing: false,
504 verify_migration: true,
505 };
506 let migrator = SessionMigrator::with_config(source, target.clone(), config);
507
508 let result = migrator.migrate().await.unwrap();
510
511 assert_eq!(result.total, 2);
514 assert_eq!(result.migrated, 2);
515 assert_eq!(result.failed, 0);
516 assert!(result.is_successful());
517 assert!(target.exists("sess_a").await.unwrap());
519 assert!(target.exists("sess_b").await.unwrap());
520 }
521
522 #[rstest]
523 #[tokio::test]
524 async fn test_dry_run_returns_count_without_migrating() {
525 let source = InMemorySessionBackend::new();
527 let target = InMemorySessionBackend::new();
528
529 let data: HashMap<String, serde_json::Value> =
530 [("key".into(), serde_json::json!("value"))].into();
531
532 source.save("dry1", &data, None).await.unwrap();
533 source.save("dry2", &data, None).await.unwrap();
534 source.save("dry3", &data, None).await.unwrap();
535
536 let migrator = SessionMigrator::new(source, target.clone());
537
538 let count = migrator.dry_run().await.unwrap();
540
541 assert_eq!(count, 3);
543 assert!(!target.exists("dry1").await.unwrap());
545 assert!(!target.exists("dry2").await.unwrap());
546 assert!(!target.exists("dry3").await.unwrap());
547 }
548}