Skip to main content

aria2_core/session/
active_session.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::time::Duration;
5
6use tokio::sync::RwLock;
7
8use super::session_serializer::{self, SessionEntry};
9use crate::request::request_group::RequestGroup;
10
11/// 活跃会话管理器 - 负责会话的加载、保存和自动保存
12pub struct ActiveSessionManager {
13    /// 会话文件路径
14    pub session_path: PathBuf,
15    /// 自动保存间隔
16    pub auto_save_interval: Duration,
17    /// 脏标记 - 标记是否有未保存的更改
18    dirty_flag: AtomicBool,
19}
20
21impl ActiveSessionManager {
22    /// 创建新的活跃会话管理器
23    ///
24    /// # 参数
25    /// - `session_path`: 会话文件保存路径
26    /// - `auto_save_interval`: 自动保存的时间间隔
27    ///
28    /// # 示例
29    /// ```ignore
30    /// let manager = ActiveSessionManager::new(
31    ///     PathBuf::from("/tmp/session.txt"),
32    ///     Duration::from_secs(60),
33    /// );
34    /// ```
35    pub fn new(session_path: PathBuf, auto_save_interval: Duration) -> Self {
36        tracing::info!(
37            "Creating ActiveSessionManager: path={}, interval={:?}",
38            session_path.display(),
39            auto_save_interval
40        );
41
42        ActiveSessionManager {
43            session_path,
44            auto_save_interval,
45            dirty_flag: AtomicBool::new(false),
46        }
47    }
48
49    /// 从文件加载会话数据
50    ///
51    /// 如果文件不存在,返回空的 Vec(不视为错误)
52    ///
53    /// # 返回值
54    /// - `Ok(Vec<SessionEntry>)`: 成功加载的会话条目列表
55    /// - `Err(String)`: 加载失败时的错误信息
56    pub async fn load_session(&self) -> Result<Vec<SessionEntry>, String> {
57        if !self.session_path.exists() {
58            tracing::debug!(
59                "Session file not found, returning empty list: {}",
60                self.session_path.display()
61            );
62            return Ok(vec![]);
63        }
64
65        match session_serializer::load_from_file(&self.session_path).await {
66            Ok(entries) => {
67                tracing::info!(
68                    "Session file loaded successfully: {}, entries: {}",
69                    self.session_path.display(),
70                    entries.len()
71                );
72                Ok(entries)
73            }
74            Err(e) => {
75                let err_msg = format!("Failed to load session file: {}", e);
76                tracing::error!("{}", err_msg);
77                Err(err_msg)
78            }
79        }
80    }
81
82    /// 保存所有下载组的状态到会话文件
83    ///
84    /// 使用原子写入策略:先写入临时文件 (.sess.tmp),然后重命名为目标文件。
85    /// 这确保在写入过程中如果发生崩溃,不会损坏原有的会话文件。
86    ///
87    /// # 参数
88    /// - `groups`: 需要保存状态的下载组列表
89    ///
90    /// # 返回值
91    /// - `Ok(usize)`: 成功保存的条目数量
92    /// - `Err(String)`: 保存失败时的错误信息
93    pub async fn save_session(
94        &self,
95        groups: &[Arc<RwLock<RequestGroup>>],
96    ) -> Result<usize, String> {
97        // 序列化所有组为 SessionEntry 列表
98        let mut entries = Vec::new();
99        for group_lock in groups {
100            let group = group_lock.read().await;
101            if let Some(entry) = session_serializer::group_to_entry(&group).await {
102                entries.push(entry);
103            }
104        }
105
106        if entries.is_empty() {
107            tracing::debug!("No active entries to save");
108            return Ok(0);
109        }
110
111        // 使用原子写入策略保存到文件
112        match session_serializer::save_to_file_with_entries(&self.session_path, &entries).await {
113            Ok(_) => {
114                tracing::info!(
115                    "Session file saved successfully: {}, entries: {}",
116                    self.session_path.display(),
117                    entries.len()
118                );
119                Ok(entries.len())
120            }
121            Err(e) => {
122                let err_msg = format!("Failed to save session file: {}", e);
123                tracing::error!("{}", err_msg);
124                Err(err_msg)
125            }
126        }
127    }
128
129    /// 标记会话为脏状态(有未保存的更改)
130    pub fn mark_dirty(&self) {
131        self.dirty_flag.store(true, Ordering::Relaxed);
132        tracing::debug!("Marking session as dirty");
133    }
134
135    /// 检查会话是否有未保存的更改
136    pub fn is_dirty(&self) -> bool {
137        self.dirty_flag.load(Ordering::Relaxed)
138    }
139
140    /// 启动自动保存后台任务
141    ///
142    /// 在后台循环中定期检查脏标记,如果有未保存的更改则自动保存。
143    /// 该方法会在后台启动一个 Tokio 任务,不会阻塞当前线程。
144    ///
145    /// # 参数
146    /// - `self`: 必须通过 Arc 包装,以便在后台任务中共享
147    /// - `groups`: 所有活动下载组的共享引用
148    ///
149    /// # 注意事项
150    /// - 此方法会启动一个无限循环的后台任务
151    /// - 只有当 dirty flag 为 true 时才会执行保存操作
152    /// - 保存成功后会清除 dirty flag
153    pub fn start_auto_save(self: &Arc<Self>, groups: Arc<RwLock<Vec<Arc<RwLock<RequestGroup>>>>>) {
154        let mgr = Arc::clone(self);
155
156        tracing::info!(
157            "Starting auto-save task, interval: {:?}",
158            mgr.auto_save_interval
159        );
160
161        tokio::spawn(async move {
162            let mut interval = tokio::time::interval(mgr.auto_save_interval);
163
164            loop {
165                interval.tick().await;
166
167                // 如果没有更改,跳过本次保存
168                if !mgr.is_dirty() {
169                    tracing::debug!("Auto-save check: no changes, skipping");
170                    continue;
171                }
172
173                tracing::debug!("Auto-save check: changes detected, starting save");
174
175                // 获取所有组的读锁
176                let groups_read = groups.read().await;
177                match mgr.save_session(&groups_read).await {
178                    Ok(n) => {
179                        tracing::debug!("Auto-save succeeded: saved {} entries", n);
180                        // 保存成功后清除脏标记
181                        mgr.dirty_flag.store(false, Ordering::Relaxed);
182                    }
183                    Err(e) => {
184                        tracing::warn!("Auto-save failed: {} (keeping dirty flag for retry)", e);
185                        // 保存失败时保留脏标记,下次继续尝试
186                    }
187                }
188            }
189        });
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use crate::request::request_group::{DownloadOptions, GroupId};
197    use tempfile::TempDir;
198
199    /// 测试 1: 验证 new() 正确创建管理器
200    #[test]
201    fn test_new_manager() {
202        let temp_dir = TempDir::new().expect("创建临时目录失败");
203        let session_path = temp_dir.path().join("session.txt");
204        let interval = Duration::from_secs(60);
205
206        let manager = ActiveSessionManager::new(session_path.clone(), interval);
207
208        assert_eq!(manager.session_path, session_path, "路径应正确设置");
209        assert_eq!(manager.auto_save_interval, interval, "间隔应正确设置");
210        assert!(!manager.is_dirty(), "新创建的管理器不应是脏状态");
211    }
212
213    /// 测试 2: 文件不存在时返回空列表
214    #[tokio::test]
215    async fn test_load_nonexistent_file_returns_empty() {
216        let temp_dir = TempDir::new().expect("创建临时目录失败");
217        let nonexistent_path = temp_dir.path().join("nonexistent_session.txt");
218
219        let manager = ActiveSessionManager::new(nonexistent_path, Duration::from_secs(60));
220        let result = manager.load_session().await;
221
222        assert!(result.is_ok(), "文件不存在不应返回错误");
223        let entries = result.unwrap();
224        assert!(entries.is_empty(), "文件不存在时应返回空列表");
225    }
226
227    /// 测试 3: 保存和加载往返测试
228    #[tokio::test]
229    async fn test_load_save_roundtrip() {
230        let temp_dir = TempDir::new().expect("创建临时目录失败");
231        let session_path = temp_dir.path().join("roundtrip_session.txt");
232
233        let manager = ActiveSessionManager::new(session_path.clone(), Duration::from_secs(60));
234
235        // 创建测试用的 RequestGroup
236        let gid1 = GroupId::new(0xd270c8a2);
237        let options1 = DownloadOptions {
238            dir: Some("/downloads".to_string()),
239            split: Some(4),
240            ..Default::default()
241        };
242        let group1 = Arc::new(RwLock::new(RequestGroup::new(
243            gid1,
244            vec!["http://example.com/file1.zip".to_string()],
245            options1,
246        )));
247
248        let gid2 = GroupId::new(0xabcdef01);
249        let group2 = Arc::new(RwLock::new(RequestGroup::new(
250            gid2,
251            vec![
252                "http://mirror.com/file2.iso".to_string(),
253                "ftp://backup.com/file2.iso".to_string(),
254            ],
255            DownloadOptions::default(),
256        )));
257
258        let groups = vec![group1, group2];
259
260        // 保存会话
261        let save_result = manager.save_session(&groups).await;
262        assert!(save_result.is_ok(), "保存应成功");
263        let saved_count = save_result.unwrap();
264        assert!(saved_count > 0, "应保存至少 1 个条目");
265
266        // 加载会话并验证
267        let load_result = manager.load_session().await;
268        assert!(load_result.is_ok(), "加载应成功");
269        let entries = load_result.unwrap();
270
271        assert_eq!(entries.len(), saved_count, "加载的条目数应与保存的一致");
272
273        // 验证数据完整性
274        assert!(
275            entries
276                .iter()
277                .any(|e| e.uris.contains(&"http://example.com/file1.zip".to_string())),
278            "应包含第一个 URI"
279        );
280        assert!(
281            entries
282                .iter()
283                .any(|e| e.uris.contains(&"http://mirror.com/file2.iso".to_string())),
284            "应包含第二个 URI"
285        );
286    }
287
288    /// 测试 4: mark_dirty 和 is_dirty 功能验证
289    #[test]
290    fn test_mark_dirty_and_check() {
291        let temp_dir = TempDir::new().expect("创建临时目录失败");
292        let session_path = temp_dir.path().join("dirty_test.txt");
293
294        let manager = ActiveSessionManager::new(session_path, Duration::from_secs(30));
295
296        // 初始状态应为干净
297        assert!(!manager.is_dirty(), "初始状态应是干净的");
298
299        // 标记为脏
300        manager.mark_dirty();
301        assert!(manager.is_dirty(), "mark_dirty 后应为脏状态");
302
303        // 再次标记(幂等性)
304        manager.mark_dirty();
305        assert!(manager.is_dirty(), "重复 mark_dirty 应保持脏状态");
306    }
307
308    /// 测试 5: 自动保存在干净状态下跳过保存
309    ///
310    /// 此测试通过短间隔验证:当 dirty=false 时,不会触发实际的保存操作
311    #[tokio::test]
312    async fn test_auto_save_skips_when_clean() {
313        let temp_dir = TempDir::new().expect("创建临时目录失败");
314        let session_path = temp_dir.path().join("auto_skip_test.txt");
315
316        let manager = Arc::new(ActiveSessionManager::new(
317            session_path.clone(),
318            Duration::from_millis(50), // 短间隔以加速测试
319        ));
320
321        let groups: Arc<RwLock<Vec<Arc<RwLock<RequestGroup>>>>> = Arc::new(RwLock::new(Vec::new()));
322
323        // 启动自动保存(此时 dirty=false)
324        manager.start_auto_save(Arc::clone(&groups));
325
326        // 等待几个 tick 周期
327        tokio::time::sleep(Duration::from_millis(200)).await;
328
329        // 验证文件未被创建(因为没有脏标记)
330        assert!(!session_path.exists(), "dirty=false 时不应创建会话文件");
331    }
332
333    /// 测试 6: 保存后文件应存在于指定路径
334    #[tokio::test]
335    async fn test_save_creates_file() {
336        let temp_dir = TempDir::new().expect("创建临时目录失败");
337        let session_path = temp_dir.path().join("file_creation_test.txt");
338
339        let manager = ActiveSessionManager::new(session_path.clone(), Duration::from_secs(60));
340
341        // 验证初始状态文件不存在
342        assert!(!session_path.exists(), "保存前文件不应存在");
343
344        // 创建测试组
345        let gid = GroupId::new(12345);
346        let group = Arc::new(RwLock::new(RequestGroup::new(
347            gid,
348            vec!["http://test.com/file.bin".to_string()],
349            DownloadOptions::default(),
350        )));
351
352        // 执行保存
353        let result = manager.save_session(&[group]).await;
354        assert!(result.is_ok(), "保存应成功");
355
356        // 验证文件已创建
357        assert!(session_path.exists(), "保存后文件应存在于指定路径");
358
359        // 验证文件内容非空
360        let content = tokio::fs::read_to_string(&session_path)
361            .await
362            .expect("读取文件失败");
363        assert!(!content.is_empty(), "文件内容不应为空");
364        assert!(
365            content.contains("http://test.com/file.bin"),
366            "文件应包含保存的 URI"
367        );
368    }
369
370    /// 测试 7: 多次保存覆盖旧文件
371    #[tokio::test]
372    async fn test_multiple_saves_overwrite() {
373        let temp_dir = TempDir::new().expect("创建临时目录失败");
374        let session_path = temp_dir.path().join("overwrite_test.txt");
375
376        let manager = ActiveSessionManager::new(session_path.clone(), Duration::from_secs(60));
377
378        // 第一次保存
379        let gid1 = GroupId::new(1);
380        let group1 = Arc::new(RwLock::new(RequestGroup::new(
381            gid1,
382            vec!["http://first.com/a.txt".to_string()],
383            DownloadOptions::default(),
384        )));
385        let result1 = manager.save_session(&[group1]).await;
386        assert!(result1.is_ok());
387
388        // 第二次保存不同的内容
389        let gid2 = GroupId::new(2);
390        let group2 = Arc::new(RwLock::new(RequestGroup::new(
391            gid2,
392            vec!["http://second.com/b.txt".to_string()],
393            DownloadOptions::default(),
394        )));
395        let result2 = manager.save_session(&[group2]).await;
396        assert!(result2.is_ok());
397
398        // 加载并验证只包含第二次的内容
399        let entries = manager.load_session().await.expect("加载失败");
400        assert_eq!(entries.len(), 1, "应只有 1 个条目(最新的)");
401        assert!(
402            entries[0]
403                .uris
404                .contains(&"http://second.com/b.txt".to_string()),
405            "应包含最新保存的 URI"
406        );
407        assert!(
408            !entries[0]
409                .uris
410                .contains(&"http://first.com/a.txt".to_string()),
411            "不应包含旧的 URI"
412        );
413    }
414
415    /// 测试 8: 空组列表保存后文件不存在或为空
416    #[tokio::test]
417    async fn test_save_empty_groups() {
418        let temp_dir = TempDir::new().expect("创建临时目录失败");
419        let session_path = temp_dir.path().join("empty_groups_test.txt");
420
421        let manager = ActiveSessionManager::new(session_path.clone(), Duration::from_secs(60));
422
423        // 保存空列表
424        let empty_groups: Vec<Arc<RwLock<RequestGroup>>> = vec![];
425        let result = manager.save_session(&empty_groups).await;
426
427        assert!(result.is_ok(), "保存空列表应成功");
428        assert_eq!(result.unwrap(), 0, "应返回 0 个条目");
429
430        // 文件可能不存在或为空(取决于实现)
431        if session_path.exists() {
432            let content = tokio::fs::read_to_string(&session_path)
433                .await
434                .expect("读取文件失败");
435            assert!(content.is_empty(), "空组列表应产生空文件");
436        }
437    }
438
439    /// 测试 9: 自动保存触发时的完整流程
440    #[tokio::test]
441    async fn test_auto_save_triggers_on_dirty() {
442        let temp_dir = TempDir::new().expect("创建临时目录失败");
443        let session_path = temp_dir.path().join("auto_trigger_test.txt");
444
445        let manager = Arc::new(ActiveSessionManager::new(
446            session_path.clone(),
447            Duration::from_millis(50), // 短间隔
448        ));
449
450        // 创建测试组
451        let gid = GroupId::new(99999);
452        let group = Arc::new(RwLock::new(RequestGroup::new(
453            gid,
454            vec!["http://auto-save-test.com/data.bin".to_string()],
455            DownloadOptions::default(),
456        )));
457
458        let groups: Arc<RwLock<Vec<Arc<RwLock<RequestGroup>>>>> =
459            Arc::new(RwLock::new(vec![group]));
460
461        // 启动自动保存
462        manager.start_auto_save(Arc::clone(&groups));
463
464        // 标记为脏
465        manager.mark_dirty();
466
467        // 等待足够的时间让自动保存执行
468        tokio::time::sleep(Duration::from_millis(300)).await;
469
470        // 验证文件已被创建(因为 dirty=true 触发了保存)
471        // 注意:由于异步任务的时序,这里可能需要更长的等待时间
472        // 我们给予合理的等待时间
473        if session_path.exists() {
474            let content = tokio::fs::read_to_string(&session_path)
475                .await
476                .expect("读取文件失败");
477            assert!(
478                content.contains("http://auto-save-test.com/data.bin") || content.is_empty(),
479                "文件应包含保存的数据或为空(取决于时序)"
480            );
481        }
482        // 即使文件尚未创建也是可接受的(取决于异步调度)
483    }
484
485    /// 测试 10: 不同 auto_save_interval 的配置
486    #[test]
487    fn test_different_intervals() {
488        let temp_dir = TempDir::new().expect("创建临时目录失败");
489
490        // 测试各种间隔配置
491        let intervals = [
492            Duration::from_secs(1),
493            Duration::from_secs(30),
494            Duration::from_secs(60),
495            Duration::from_secs(300),
496            Duration::from_millis(500),
497        ];
498
499        for (i, interval) in intervals.iter().enumerate() {
500            let path = temp_dir.path().join(format!("interval_test_{}.txt", i));
501            let manager = ActiveSessionManager::new(path, *interval);
502            assert_eq!(
503                manager.auto_save_interval, *interval,
504                "间隔 {} 应正确设置",
505                i
506            );
507        }
508    }
509}