Skip to main content

aster_forge_tasks/
temp.rs

1//! Temporary directory helpers for background task artifacts.
2//!
3//! Task workers often write intermediate files under a token-scoped directory. These helpers keep
4//! the shared directory layout and cleanup behavior in Forge while products keep ownership of when
5//! a task is allowed to create or delete artifacts.
6
7use crate::{Result, TaskCoreError, TaskLease};
8
9/// Cleans a temporary directory tree, logging failures instead of returning them.
10///
11/// Missing directories are accepted. `DirectoryNotEmpty` is retried because some platforms and
12/// filesystem watchers can briefly create files while a recursive removal is in progress.
13pub async fn cleanup_temp_dir(path: &str) {
14    aster_forge_utils::fs::cleanup_temp_dir(path).await;
15}
16
17/// Cleans the short-lived runtime temporary directory under `temp_root`.
18pub async fn cleanup_runtime_temp_root(temp_root: &str) {
19    aster_forge_utils::fs::cleanup_runtime_temp_root(temp_root).await;
20}
21
22/// Prepares the token-scoped temporary directory for one claimed task lease.
23pub async fn prepare_task_temp_dir_in_root(temp_root: &str, lease: TaskLease) -> Result<String> {
24    tracing::debug!(
25        task_id = lease.task_id,
26        processing_token = lease.processing_token,
27        "preparing background task temp dir"
28    );
29    cleanup_task_temp_dir_for_lease_in_root(temp_root, lease).await?;
30    let task_temp_dir = aster_forge_utils::paths::task_token_temp_dir(
31        temp_root,
32        lease.task_id,
33        lease.processing_token,
34    );
35    tokio::fs::create_dir_all(&task_temp_dir)
36        .await
37        .map_err(|error| TaskCoreError::io(format!("create task temp dir: {error}")))?;
38    tracing::debug!(
39        task_id = lease.task_id,
40        processing_token = lease.processing_token,
41        "prepared background task temp dir"
42    );
43    Ok(task_temp_dir)
44}
45
46/// Cleans the token-scoped temporary directory for one claimed task lease.
47pub async fn cleanup_task_temp_dir_for_lease_in_root(
48    temp_root: &str,
49    lease: TaskLease,
50) -> Result<()> {
51    tracing::debug!(
52        task_id = lease.task_id,
53        processing_token = lease.processing_token,
54        "cleaning background task temp dir for lease"
55    );
56    cleanup_temp_dir(&aster_forge_utils::paths::task_token_temp_dir(
57        temp_root,
58        lease.task_id,
59        lease.processing_token,
60    ))
61    .await;
62    Ok(())
63}
64
65/// Cleans every temporary artifact directory for one persisted task id.
66pub async fn cleanup_task_temp_dir_for_task_in_root(temp_root: &str, task_id: i64) -> Result<()> {
67    tracing::debug!(task_id, "cleaning background task temp dir in root");
68    cleanup_temp_dir(&aster_forge_utils::paths::task_temp_dir(temp_root, task_id)).await;
69    Ok(())
70}
71
72#[cfg(test)]
73mod tests {
74    use std::path::PathBuf;
75    use std::sync::atomic::{AtomicU64, Ordering};
76
77    use crate::TaskLease;
78
79    use super::{
80        cleanup_runtime_temp_root, cleanup_task_temp_dir_for_lease_in_root,
81        cleanup_task_temp_dir_for_task_in_root, cleanup_temp_dir, prepare_task_temp_dir_in_root,
82    };
83
84    static TEMP_ID: AtomicU64 = AtomicU64::new(0);
85
86    fn unique_temp_path(label: &str) -> PathBuf {
87        std::env::temp_dir().join(format!(
88            "aster-forge-tasks-{label}-{}-{}",
89            std::process::id(),
90            TEMP_ID.fetch_add(1, Ordering::Relaxed)
91        ))
92    }
93
94    #[tokio::test]
95    async fn cleanup_temp_dir_removes_directory_tree() {
96        let path = unique_temp_path("cleanup");
97        let nested = path.join("nested");
98        tokio::fs::create_dir_all(&nested).await.unwrap();
99        tokio::fs::write(nested.join("payload.txt"), b"temporary")
100            .await
101            .unwrap();
102
103        cleanup_temp_dir(path.to_str().unwrap()).await;
104
105        assert!(!path.exists());
106    }
107
108    #[tokio::test]
109    async fn cleanup_temp_dir_tolerates_missing_directory() {
110        let path = unique_temp_path("missing-cleanup");
111
112        cleanup_temp_dir(path.to_str().unwrap()).await;
113
114        assert!(!path.exists());
115    }
116
117    #[tokio::test]
118    async fn prepare_task_temp_dir_creates_token_scoped_directory() {
119        let root = unique_temp_path("prepare");
120        let lease = TaskLease::new(42, 7);
121
122        let prepared = prepare_task_temp_dir_in_root(root.to_str().unwrap(), lease)
123            .await
124            .expect("task temp dir should be prepared");
125
126        assert!(PathBuf::from(&prepared).is_dir());
127        cleanup_temp_dir(root.to_str().unwrap()).await;
128    }
129
130    #[tokio::test]
131    async fn cleanup_task_temp_dir_for_lease_removes_only_token_dir() {
132        let root = unique_temp_path("lease-cleanup");
133        let lease = TaskLease::new(42, 7);
134        let keep = aster_forge_utils::paths::task_token_temp_dir(root.to_str().unwrap(), 42, 8);
135        let remove = prepare_task_temp_dir_in_root(root.to_str().unwrap(), lease)
136            .await
137            .expect("task temp dir should be prepared");
138        tokio::fs::create_dir_all(&keep).await.unwrap();
139
140        cleanup_task_temp_dir_for_lease_in_root(root.to_str().unwrap(), lease)
141            .await
142            .expect("lease cleanup should succeed");
143
144        assert!(!PathBuf::from(remove).exists());
145        assert!(PathBuf::from(&keep).is_dir());
146        cleanup_temp_dir(root.to_str().unwrap()).await;
147    }
148
149    #[tokio::test]
150    async fn cleanup_task_temp_dir_for_task_removes_all_token_dirs() {
151        let root = unique_temp_path("task-cleanup");
152        let lease = TaskLease::new(42, 7);
153        prepare_task_temp_dir_in_root(root.to_str().unwrap(), lease)
154            .await
155            .expect("task temp dir should be prepared");
156
157        cleanup_task_temp_dir_for_task_in_root(root.to_str().unwrap(), 42)
158            .await
159            .expect("task cleanup should succeed");
160
161        assert!(
162            !PathBuf::from(aster_forge_utils::paths::task_temp_dir(
163                root.to_str().unwrap(),
164                42
165            ))
166            .exists()
167        );
168        cleanup_temp_dir(root.to_str().unwrap()).await;
169    }
170
171    #[tokio::test]
172    async fn cleanup_runtime_temp_root_removes_runtime_namespace_only() {
173        let root = unique_temp_path("runtime-cleanup");
174        let runtime = aster_forge_utils::paths::runtime_temp_dir(root.to_str().unwrap());
175        let keep = root.join("tasks");
176        tokio::fs::create_dir_all(&runtime).await.unwrap();
177        tokio::fs::create_dir_all(&keep).await.unwrap();
178
179        cleanup_runtime_temp_root(root.to_str().unwrap()).await;
180
181        assert!(!PathBuf::from(runtime).exists());
182        assert!(keep.is_dir());
183        cleanup_temp_dir(root.to_str().unwrap()).await;
184    }
185}