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.
23///
24/// # Errors
25///
26/// Returns [`TaskCoreError`] when the lease path is invalid or directory creation fails.
27pub async fn prepare_task_temp_dir_in_root(temp_root: &str, lease: TaskLease) -> Result<String> {
28    tracing::debug!(
29        task_id = lease.task_id,
30        processing_token = lease.processing_token,
31        "preparing background task temp dir"
32    );
33    cleanup_task_temp_dir_for_lease_in_root(temp_root, lease).await?;
34    let task_temp_dir = aster_forge_utils::paths::task_token_temp_dir(
35        temp_root,
36        lease.task_id,
37        lease.processing_token,
38    );
39    tokio::fs::create_dir_all(&task_temp_dir)
40        .await
41        .map_err(|error| TaskCoreError::io(format!("create task temp dir: {error}")))?;
42    tracing::debug!(
43        task_id = lease.task_id,
44        processing_token = lease.processing_token,
45        "prepared background task temp dir"
46    );
47    Ok(task_temp_dir)
48}
49
50/// Cleans the token-scoped temporary directory for one claimed task lease.
51///
52/// # Errors
53///
54/// Returns [`TaskCoreError`] when the lease path is invalid or cleanup fails.
55pub async fn cleanup_task_temp_dir_for_lease_in_root(
56    temp_root: &str,
57    lease: TaskLease,
58) -> Result<()> {
59    tracing::debug!(
60        task_id = lease.task_id,
61        processing_token = lease.processing_token,
62        "cleaning background task temp dir for lease"
63    );
64    cleanup_temp_dir(&aster_forge_utils::paths::task_token_temp_dir(
65        temp_root,
66        lease.task_id,
67        lease.processing_token,
68    ))
69    .await;
70    Ok(())
71}
72
73/// Cleans every temporary artifact directory for one persisted task id.
74///
75/// # Errors
76///
77/// Returns [`TaskCoreError`] when the task path is invalid or cleanup fails.
78pub async fn cleanup_task_temp_dir_for_task_in_root(temp_root: &str, task_id: i64) -> Result<()> {
79    tracing::debug!(task_id, "cleaning background task temp dir in root");
80    cleanup_temp_dir(&aster_forge_utils::paths::task_temp_dir(temp_root, task_id)).await;
81    Ok(())
82}
83
84#[cfg(test)]
85mod tests {
86    use std::path::PathBuf;
87    use std::sync::atomic::{AtomicU64, Ordering};
88
89    use crate::TaskLease;
90
91    use super::{
92        cleanup_runtime_temp_root, cleanup_task_temp_dir_for_lease_in_root,
93        cleanup_task_temp_dir_for_task_in_root, cleanup_temp_dir, prepare_task_temp_dir_in_root,
94    };
95
96    static TEMP_ID: AtomicU64 = AtomicU64::new(0);
97
98    fn unique_temp_path(label: &str) -> PathBuf {
99        std::env::temp_dir().join(format!(
100            "aster-forge-tasks-{label}-{}-{}",
101            std::process::id(),
102            TEMP_ID.fetch_add(1, Ordering::Relaxed)
103        ))
104    }
105
106    #[tokio::test]
107    async fn cleanup_temp_dir_removes_directory_tree() {
108        let path = unique_temp_path("cleanup");
109        let nested = path.join("nested");
110        tokio::fs::create_dir_all(&nested).await.unwrap();
111        tokio::fs::write(nested.join("payload.txt"), b"temporary")
112            .await
113            .unwrap();
114
115        cleanup_temp_dir(path.to_str().unwrap()).await;
116
117        assert!(!path.exists());
118    }
119
120    #[tokio::test]
121    async fn cleanup_temp_dir_tolerates_missing_directory() {
122        let path = unique_temp_path("missing-cleanup");
123
124        cleanup_temp_dir(path.to_str().unwrap()).await;
125
126        assert!(!path.exists());
127    }
128
129    #[tokio::test]
130    async fn prepare_task_temp_dir_creates_token_scoped_directory() {
131        let root = unique_temp_path("prepare");
132        let lease = TaskLease::new(42, 7);
133
134        let prepared = prepare_task_temp_dir_in_root(root.to_str().unwrap(), lease)
135            .await
136            .expect("task temp dir should be prepared");
137
138        assert!(PathBuf::from(&prepared).is_dir());
139        cleanup_temp_dir(root.to_str().unwrap()).await;
140    }
141
142    #[tokio::test]
143    async fn cleanup_task_temp_dir_for_lease_removes_only_token_dir() {
144        let root = unique_temp_path("lease-cleanup");
145        let lease = TaskLease::new(42, 7);
146        let keep = aster_forge_utils::paths::task_token_temp_dir(root.to_str().unwrap(), 42, 8);
147        let remove = prepare_task_temp_dir_in_root(root.to_str().unwrap(), lease)
148            .await
149            .expect("task temp dir should be prepared");
150        tokio::fs::create_dir_all(&keep).await.unwrap();
151
152        cleanup_task_temp_dir_for_lease_in_root(root.to_str().unwrap(), lease)
153            .await
154            .expect("lease cleanup should succeed");
155
156        assert!(!PathBuf::from(remove).exists());
157        assert!(PathBuf::from(&keep).is_dir());
158        cleanup_temp_dir(root.to_str().unwrap()).await;
159    }
160
161    #[tokio::test]
162    async fn cleanup_task_temp_dir_for_task_removes_all_token_dirs() {
163        let root = unique_temp_path("task-cleanup");
164        let lease = TaskLease::new(42, 7);
165        prepare_task_temp_dir_in_root(root.to_str().unwrap(), lease)
166            .await
167            .expect("task temp dir should be prepared");
168
169        cleanup_task_temp_dir_for_task_in_root(root.to_str().unwrap(), 42)
170            .await
171            .expect("task cleanup should succeed");
172
173        assert!(
174            !PathBuf::from(aster_forge_utils::paths::task_temp_dir(
175                root.to_str().unwrap(),
176                42
177            ))
178            .exists()
179        );
180        cleanup_temp_dir(root.to_str().unwrap()).await;
181    }
182
183    #[tokio::test]
184    async fn cleanup_runtime_temp_root_removes_runtime_namespace_only() {
185        let root = unique_temp_path("runtime-cleanup");
186        let runtime = aster_forge_utils::paths::runtime_temp_dir(root.to_str().unwrap());
187        let keep = root.join("tasks");
188        tokio::fs::create_dir_all(&runtime).await.unwrap();
189        tokio::fs::create_dir_all(&keep).await.unwrap();
190
191        cleanup_runtime_temp_root(root.to_str().unwrap()).await;
192
193        assert!(!PathBuf::from(runtime).exists());
194        assert!(keep.is_dir());
195        cleanup_temp_dir(root.to_str().unwrap()).await;
196    }
197}