Skip to main content

reinhardt_auth/sessions/
migration.rs

1//! Session storage migration tools
2//!
3//! This module provides tools for migrating session data between different backends.
4//!
5//! ## Example
6//!
7//! ```rust,no_run,ignore
8//! use reinhardt_auth::sessions::migration::{SessionMigrator, Migrator};
9//! use reinhardt_auth::sessions::backends::{InMemorySessionBackend};
10//!
11//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
12//! let source_backend = InMemorySessionBackend::new();
13//! let target_backend = InMemorySessionBackend::new();
14//!
15//! // Create migrator
16//! let migrator = SessionMigrator::new(source_backend, target_backend);
17//!
18//! // Run migration
19//! let result = migrator.migrate().await?;
20//! println!("Migrated {} sessions, {} failed", result.migrated, result.failed);
21//! # Ok(())
22//! # }
23//! ```
24
25use super::backends::{SessionBackend, SessionError};
26use super::cleanup::CleanupableBackend;
27use async_trait::async_trait;
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30
31/// Migration result
32///
33/// # Example
34///
35/// ```rust
36/// use reinhardt_auth::sessions::migration::MigrationResult;
37///
38/// let result = MigrationResult {
39///     total: 100,
40///     migrated: 95,
41///     failed: 5,
42///     errors: vec!["Key 'abc' failed: timeout".to_string()],
43/// };
44///
45/// assert_eq!(result.total, 100);
46/// assert_eq!(result.migrated, 95);
47/// ```
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct MigrationResult {
50	/// Total number of sessions to migrate
51	pub total: usize,
52	/// Number of sessions successfully migrated
53	pub migrated: usize,
54	/// Number of sessions that failed to migrate
55	pub failed: usize,
56	/// List of error messages
57	pub errors: Vec<String>,
58}
59
60impl MigrationResult {
61	/// Create a new empty migration result
62	///
63	/// # Example
64	///
65	/// ```rust
66	/// use reinhardt_auth::sessions::migration::MigrationResult;
67	///
68	/// let result = MigrationResult::new();
69	/// assert_eq!(result.total, 0);
70	/// assert_eq!(result.migrated, 0);
71	/// ```
72	pub fn new() -> Self {
73		Self {
74			total: 0,
75			migrated: 0,
76			failed: 0,
77			errors: Vec::new(),
78		}
79	}
80
81	/// Check if migration was successful
82	///
83	/// # Example
84	///
85	/// ```rust
86	/// use reinhardt_auth::sessions::migration::MigrationResult;
87	///
88	/// let mut result = MigrationResult::new();
89	/// result.total = 10;
90	/// result.migrated = 10;
91	/// assert!(result.is_successful());
92	///
93	/// result.failed = 1;
94	/// assert!(!result.is_successful());
95	/// ```
96	pub fn is_successful(&self) -> bool {
97		self.failed == 0
98	}
99
100	/// Get success rate as percentage
101	///
102	/// # Example
103	///
104	/// ```rust
105	/// use reinhardt_auth::sessions::migration::MigrationResult;
106	///
107	/// let mut result = MigrationResult::new();
108	/// result.total = 100;
109	/// result.migrated = 95;
110	/// result.failed = 5;
111	///
112	/// assert_eq!(result.success_rate(), 95.0);
113	/// ```
114	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/// Migration configuration
129///
130/// # Example
131///
132/// ```rust
133/// use reinhardt_auth::sessions::migration::MigrationConfig;
134///
135/// let config = MigrationConfig {
136///     batch_size: 100,
137///     skip_existing: true,
138///     verify_migration: false,
139/// };
140/// ```
141#[derive(Debug, Clone)]
142pub struct MigrationConfig {
143	/// Number of sessions to migrate in one batch
144	pub batch_size: usize,
145	/// Skip sessions that already exist in target
146	pub skip_existing: bool,
147	/// Verify each migration by reading back from target
148	pub verify_migration: bool,
149}
150
151impl Default for MigrationConfig {
152	/// Create default migration configuration
153	///
154	/// # Example
155	///
156	/// ```rust
157	/// use reinhardt_auth::sessions::migration::MigrationConfig;
158	///
159	/// let config = MigrationConfig::default();
160	/// assert_eq!(config.batch_size, 1000);
161	/// assert!(!config.skip_existing);
162	/// assert!(!config.verify_migration);
163	/// ```
164	fn default() -> Self {
165		Self {
166			batch_size: 1000,
167			skip_existing: false,
168			verify_migration: false,
169		}
170	}
171}
172
173/// Session migrator trait
174#[async_trait]
175pub trait Migrator {
176	/// Run migration
177	async fn migrate(&self) -> Result<MigrationResult, SessionError>;
178
179	/// Dry run migration (count only, no actual migration)
180	async fn dry_run(&self) -> Result<usize, SessionError>;
181}
182
183/// Session migrator for transferring sessions between backends
184///
185/// # Example
186///
187/// ```rust,no_run
188/// # #[tokio::main]
189/// # async fn main() {
190/// use reinhardt_auth::sessions::migration::{SessionMigrator, MigrationConfig, Migrator};
191/// use reinhardt_auth::sessions::backends::InMemorySessionBackend;
192///
193/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
194/// let source = InMemorySessionBackend::new();
195/// let target = InMemorySessionBackend::new();
196///
197/// let config = MigrationConfig::default();
198/// let migrator = SessionMigrator::with_config(source, target, config);
199///
200/// let result = migrator.migrate().await?;
201/// println!("Migration complete: {} sessions migrated", result.migrated);
202/// # Ok(())
203/// # }
204/// # }
205/// ```
206pub 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	/// Create a new session migrator with default configuration
214	///
215	/// # Example
216	///
217	/// ```rust
218	/// use reinhardt_auth::sessions::migration::SessionMigrator;
219	/// use reinhardt_auth::sessions::backends::InMemorySessionBackend;
220	///
221	/// let source = InMemorySessionBackend::new();
222	/// let target = InMemorySessionBackend::new();
223	/// let migrator = SessionMigrator::new(source, target);
224	/// ```
225	pub fn new(source: S, target: T) -> Self {
226		Self {
227			source,
228			target,
229			config: MigrationConfig::default(),
230		}
231	}
232
233	/// Create a new session migrator with custom configuration
234	///
235	/// # Example
236	///
237	/// ```rust
238	/// use reinhardt_auth::sessions::migration::{SessionMigrator, MigrationConfig};
239	/// use reinhardt_auth::sessions::backends::InMemorySessionBackend;
240	///
241	/// let source = InMemorySessionBackend::new();
242	/// let target = InMemorySessionBackend::new();
243	/// let config = MigrationConfig {
244	///     batch_size: 500,
245	///     skip_existing: true,
246	///     verify_migration: true,
247	/// };
248	/// let migrator = SessionMigrator::with_config(source, target, config);
249	/// ```
250	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		// Get all session keys from source
269		let all_keys = self.source.get_all_keys().await?;
270		result.total = all_keys.len();
271
272		// Migrate in batches
273		for chunk in all_keys.chunks(self.config.batch_size) {
274			for key in chunk {
275				// Skip if exists and configured to skip
276				if self.config.skip_existing && self.target.exists(key).await? {
277					continue;
278				}
279
280				// Load from source
281				match self
282					.source
283					.load::<HashMap<String, serde_json::Value>>(key)
284					.await
285				{
286					Ok(Some(data)) => {
287						// Save to target
288						match self.target.save(key, &data, None).await {
289							Ok(_) => {
290								// Verify if configured
291								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						// Session doesn't exist in source, skip
327					}
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		// Arrange
406		let source = InMemorySessionBackend::new();
407		let target = InMemorySessionBackend::new();
408		let migrator = SessionMigrator::new(source, target);
409
410		// Act
411		let result = migrator.migrate().await.unwrap();
412
413		// Assert
414		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		// Arrange
425		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		// Act
442		let result = migrator.migrate().await.unwrap();
443
444		// Assert
445		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		// Arrange
457		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		// Act
476		let result = migrator.migrate().await.unwrap();
477
478		// Assert
479		assert_eq!(result.total, 1);
480		// The session was skipped, so migrated count should be 0
481		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		// Arrange
490		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		// Act
509		let result = migrator.migrate().await.unwrap();
510
511		// Assert
512		// With verification enabled, migrated count reflects verified sessions
513		assert_eq!(result.total, 2);
514		assert_eq!(result.migrated, 2);
515		assert_eq!(result.failed, 0);
516		assert!(result.is_successful());
517		// Verify data actually exists in target
518		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		// Arrange
526		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		// Act
539		let count = migrator.dry_run().await.unwrap();
540
541		// Assert
542		assert_eq!(count, 3);
543		// Target must remain empty after dry run
544		assert!(!target.exists("dry1").await.unwrap());
545		assert!(!target.exists("dry2").await.unwrap());
546		assert!(!target.exists("dry3").await.unwrap());
547	}
548}