object_store/
temp_file.rs1use std::time::Duration;
18
19use common_telemetry::warn;
20use opendal::raw::Timestamp;
21use uuid::Uuid;
22
23use crate::{ErrorKind, ObjectStore};
24
25const TEMPORARY_PREFIX: &str = ".";
27
28const TEMPORARY_MARKER: &str = ".greptime-";
30
31const TEMPORARY_SUFFIX: &str = ".tmp";
33
34const TEMPORARY_FILE_SCHEME: &str = crate::services::HDFS_NATIVE_SCHEME;
36
37pub fn writes_temporary_files(store: &ObjectStore) -> bool {
43 store.info().scheme() == TEMPORARY_FILE_SCHEME
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Default)]
52pub struct SweepReport {
53 pub deleted: usize,
55 pub failed: usize,
57}
58
59impl SweepReport {
60 fn merge(&mut self, other: SweepReport) {
61 self.deleted += other.deleted;
62 self.failed += other.failed;
63 }
64}
65
66pub async fn sweep_temporary_files(
77 store: &ObjectStore,
78 dirs: &[String],
79 older_than: Duration,
80) -> SweepReport {
81 let mut report = SweepReport::default();
82 for dir in dirs {
83 report.merge(sweep_dir(store, dir, older_than).await);
84 }
85 report
86}
87
88async fn sweep_dir(store: &ObjectStore, dir: &str, older_than: Duration) -> SweepReport {
89 let entries = match store.list(dir).await {
90 Ok(entries) => entries,
91 Err(error) if error.kind() == ErrorKind::NotFound => return SweepReport::default(),
94 Err(error) => {
95 warn!(error; "Failed to list {dir} for temporary files");
96 return SweepReport {
97 failed: 1,
98 ..Default::default()
99 };
100 }
101 };
102
103 let expired_before = Timestamp::now()
104 .into_inner()
105 .as_millisecond()
106 .saturating_sub(i64::try_from(older_than.as_millis()).unwrap_or(i64::MAX));
107
108 let mut report = SweepReport::default();
109 for entry in entries {
110 if !is_temporary_file(entry.name()) {
111 continue;
112 }
113
114 let is_expired = entry
118 .metadata()
119 .last_modified()
120 .map(|last_modified| last_modified.into_inner().as_millisecond() < expired_before)
121 .unwrap_or(false);
124 if !is_expired {
125 continue;
126 }
127
128 let path = entry.path();
132 match store.delete(path).await {
133 Ok(()) => report.deleted += 1,
134 Err(error) => {
135 warn!(error; "Failed to delete temporary file {path}");
136 report.failed += 1;
137 }
138 }
139 }
140
141 report
142}
143
144pub(crate) fn temporary_path(path: &str) -> String {
149 let suffix = format!("{TEMPORARY_MARKER}{}{TEMPORARY_SUFFIX}", Uuid::new_v4());
150 match path.rsplit_once('/') {
151 Some((parent, name)) => format!("{parent}/{TEMPORARY_PREFIX}{name}{suffix}"),
152 None => format!("{TEMPORARY_PREFIX}{path}{suffix}"),
153 }
154}
155
156fn is_temporary_file(name: &str) -> bool {
157 name.starts_with(TEMPORARY_PREFIX)
159 && name.contains(TEMPORARY_MARKER)
160 && name.ends_with(TEMPORARY_SUFFIX)
161}
162
163#[cfg(test)]
164mod tests {
165 use std::sync::Arc;
166
167 use opendal::Operator;
168 use opendal::services::Fs;
169 use tempfile::TempDir;
170
171 use super::*;
172 use crate::layers::HdfsCompatibilityLayer;
173 use crate::layers::mock::{MockLayer, MockLayerBuilder, OpDelete, oio};
174 use crate::{Error, Result};
175
176 const EXPIRED: Duration = Duration::from_millis(1);
177
178 fn test_store() -> (TempDir, ObjectStore) {
179 let directory = tempfile::tempdir().unwrap();
180 let store = Operator::new(Fs::default().root(directory.path().to_str().unwrap())).unwrap();
181 (directory, store)
182 }
183
184 fn test_store_of(layer: MockLayer) -> (TempDir, ObjectStore) {
185 let directory = tempfile::tempdir().unwrap();
186 let store = Operator::new(Fs::default().root(directory.path().to_str().unwrap()))
187 .unwrap()
188 .layer(layer);
189 (directory, store)
190 }
191
192 fn test_store_of_failing_list(kind: ErrorKind) -> (TempDir, ObjectStore) {
193 struct FailingLister(ErrorKind);
194
195 impl oio::List for FailingLister {
196 async fn next(&mut self) -> Result<Option<oio::Entry>> {
197 Err(Error::new(self.0, "Failed to list"))
198 }
199 }
200
201 let layer = MockLayerBuilder::default()
202 .lister_factory(Arc::new(move |_, _, _| Box::new(FailingLister(kind))))
203 .build()
204 .unwrap();
205 test_store_of(layer)
206 }
207
208 fn test_store_of_failing_delete(marker: &'static str) -> (TempDir, ObjectStore) {
210 struct FailingDeleter {
211 inner: oio::Deleter,
212 marker: &'static str,
213 }
214
215 impl oio::Delete for FailingDeleter {
216 async fn delete(&mut self, path: &str, args: OpDelete) -> Result<()> {
217 if path.contains(self.marker) {
218 return Err(Error::new(ErrorKind::Unexpected, "Failed to delete"));
219 }
220 self.inner.delete(path, args).await
221 }
222
223 async fn close(&mut self) -> Result<()> {
224 self.inner.close().await
225 }
226 }
227
228 let layer = MockLayerBuilder::default()
229 .deleter_factory(Arc::new(move |inner| {
230 Box::new(FailingDeleter { inner, marker })
231 }))
232 .build()
233 .unwrap();
234 test_store_of(layer)
235 }
236
237 async fn wait_until_expired() {
238 tokio::time::sleep(Duration::from_millis(10)).await;
242 }
243
244 #[test]
245 fn test_is_temporary_file() {
246 assert!(is_temporary_file(
247 ".00000000000000000042.json.greptime-53f0f4a6-3f3e-4a2f-9a2f-6fe4b0d5b5b0.tmp"
248 ));
249 assert!(is_temporary_file(".a5f2.parquet.greptime-53f0f4a6.tmp"));
250
251 assert!(!is_temporary_file("00000000000000000042.json"));
252 assert!(!is_temporary_file(".00000000000000000042.json"));
253 assert!(!is_temporary_file(
254 "00000000000000000042.json.greptime-53f0f4a6.tmp"
255 ));
256 assert!(!is_temporary_file(
257 ".00000000000000000042.json.greptime-53f0f4a6"
258 ));
259 assert!(!is_temporary_file("index/"));
260 }
261
262 #[tokio::test]
263 async fn test_sweep_dir_deletes_expired_temporary_file() {
264 let (_directory, store) = test_store();
265 let temporary = "data/.42.parquet.greptime-53f0f4a6.tmp";
266 store.create_dir("data/").await.unwrap();
267 store.write(temporary, "partial").await.unwrap();
268 store.write("data/42.parquet", "data").await.unwrap();
269
270 wait_until_expired().await;
271 assert_eq!(
272 SweepReport {
273 deleted: 1,
274 failed: 0
275 },
276 sweep_dir(&store, "data/", EXPIRED).await
277 );
278
279 assert!(!store.exists(temporary).await.unwrap());
280 assert!(store.exists("data/42.parquet").await.unwrap());
281 }
282
283 #[tokio::test]
284 async fn test_sweep_dir_keeps_young_temporary_file() {
285 let (_directory, store) = test_store();
286 let temporary = "data/.42.parquet.greptime-53f0f4a6.tmp";
287 store.create_dir("data/").await.unwrap();
288 store.write(temporary, "partial").await.unwrap();
289
290 assert_eq!(
291 SweepReport::default(),
292 sweep_dir(&store, "data/", Duration::from_secs(24 * 60 * 60)).await
293 );
294
295 assert!(store.exists(temporary).await.unwrap());
296 }
297
298 #[tokio::test]
299 async fn test_sweep_dir_of_missing_directory() {
300 let (_directory, store) = test_store_of_failing_list(ErrorKind::NotFound);
303
304 assert_eq!(
307 SweepReport::default(),
308 sweep_dir(&store, "data/staging/blob/", Duration::ZERO).await
309 );
310 }
311
312 #[tokio::test]
313 async fn test_sweep_dir_reports_failed_list() {
314 let (_directory, store) = test_store_of_failing_list(ErrorKind::Unexpected);
315
316 assert_eq!(
317 SweepReport {
318 deleted: 0,
319 failed: 1
320 },
321 sweep_dir(&store, "data/", Duration::ZERO).await
322 );
323 }
324
325 #[tokio::test]
326 async fn test_sweep_dir_keeps_going_after_failed_delete() {
327 let (_directory, store) = test_store_of_failing_delete("fail");
328 let failing = "data/.fail-42.parquet.greptime-53f0f4a6.tmp";
329 let sweepable = "data/.ok-42.parquet.greptime-53f0f4a6.tmp";
330 store.create_dir("data/").await.unwrap();
331 store.write(failing, "partial").await.unwrap();
332 store.write(sweepable, "partial").await.unwrap();
333
334 wait_until_expired().await;
335 assert_eq!(
336 SweepReport {
337 deleted: 1,
338 failed: 1
339 },
340 sweep_dir(&store, "data/", EXPIRED).await
341 );
342
343 assert!(store.exists(failing).await.unwrap());
344 assert!(!store.exists(sweepable).await.unwrap());
345 }
346
347 #[tokio::test]
348 async fn test_sweep_temporary_files_on_compatibility_layer() {
349 let (_directory, store) = test_store();
350 let temporary = "data/.42.parquet.greptime-53f0f4a6.tmp";
351 store.create_dir("data/").await.unwrap();
352 store.write(temporary, "partial").await.unwrap();
354 let store = store.layer(HdfsCompatibilityLayer::new_for_test());
356
357 wait_until_expired().await;
358 assert_eq!(
359 SweepReport {
360 deleted: 1,
361 failed: 0
362 },
363 sweep_temporary_files(
364 &store,
365 &["data/".to_string(), "data/staging/blob/".to_string()],
366 EXPIRED,
367 )
368 .await
369 );
370
371 assert!(!store.exists(temporary).await.unwrap());
372 }
373}