witchcraft_server/logging/logger/
rolling_file.rs

1// Copyright 2021 Palantir Technologies, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14use crate::logging::logger::byte_buffer::BufBytesSink;
15use async_compression::tokio::write::GzipEncoder;
16use bytes::{Buf, Bytes};
17use conjure_error::Error;
18use conjure_object::chrono::NaiveDate;
19use conjure_object::Utc;
20use futures_sink::Sink;
21use futures_util::ready;
22use pin_project::pin_project;
23use regex::Regex;
24use std::future::Future;
25use std::path::{Path, PathBuf};
26use std::pin::Pin;
27use std::sync::Arc;
28use std::task::{Context, Poll};
29use tokio::fs::{self, File, OpenOptions};
30use tokio::io::{self, AsyncWrite, AsyncWriteExt};
31use tokio::task;
32
33const MAX_LOG_SIZE: u64 = 1024 * 1024 * 1024;
34
35struct CurrentFile {
36    sink: BufBytesSink<FileBytesSink>,
37    len: u64,
38    date: NaiveDate,
39}
40
41impl CurrentFile {
42    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
43        Pin::new(&mut self.sink).poll_ready(cx)
44    }
45
46    fn start_send(&mut self, item: Bytes) -> io::Result<()> {
47        self.len += item.remaining() as u64;
48        Pin::new(&mut self.sink).start_send(item)
49    }
50
51    fn poll_flush(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
52        Pin::new(&mut self.sink).poll_flush(cx)
53    }
54
55    fn poll_close(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
56        Pin::new(&mut self.sink).poll_close(cx)
57    }
58}
59
60#[allow(clippy::large_enum_variant)]
61enum State {
62    Live(CurrentFile),
63    Rotating(Pin<Box<dyn Future<Output = io::Result<File>> + Sync + Send>>),
64}
65
66pub struct RollingFileAppender {
67    state: State,
68    next_archive_index: u32,
69    name: &'static str,
70    max_archive_size: u64,
71    max_archive_days: u32,
72    archive_locator: Arc<ArchiveLocator>,
73}
74
75impl RollingFileAppender {
76    pub async fn new(
77        name: &'static str,
78        size_limit_gb: u32,
79        max_archive_days: u32,
80    ) -> Result<Self, Error> {
81        let max_archive_size = u64::from(size_limit_gb) * 1024 * 1024 * 1024;
82
83        let dir = log_dir();
84        fs::create_dir_all(&dir)
85            .await
86            .map_err(Error::internal_safe)?;
87        let file_path = log_path(dir, name);
88        let file = open_log(&file_path).await.map_err(Error::internal_safe)?;
89        let len = file.metadata().await.map_err(Error::internal_safe)?.len();
90
91        let archive_locator = ArchiveLocator::new(name);
92        let date = Utc::now().date_naive();
93
94        let next_archive_index = archive_locator
95            .archived_logs(dir)
96            .await
97            .map_err(Error::internal_safe)?
98            .iter()
99            .chain(
100                archive_locator
101                    .uncompressed_logs(dir)
102                    .await
103                    .map_err(Error::internal_safe)?
104                    .iter(),
105            )
106            .filter(|l| l.date == date)
107            .map(|l| l.number)
108            .max()
109            .map_or(0, |n| n + 1);
110
111        clear_old_archives(
112            dir,
113            date,
114            max_archive_size,
115            max_archive_days,
116            &archive_locator,
117        )
118        .await
119        .map_err(Error::internal_safe)?;
120
121        clear_tmp_files(dir, &archive_locator)
122            .await
123            .map_err(Error::internal_safe)?;
124        restart_compression(dir, name, &archive_locator)
125            .await
126            .map_err(Error::internal_safe)?;
127
128        Ok(RollingFileAppender {
129            state: State::Live(CurrentFile {
130                sink: BufBytesSink::new(FileBytesSink::new(file)),
131                len,
132                date,
133            }),
134            next_archive_index,
135            name,
136            max_archive_size,
137            max_archive_days,
138            archive_locator: Arc::new(archive_locator),
139        })
140    }
141}
142
143impl Sink<Bytes> for RollingFileAppender {
144    type Error = io::Error;
145
146    fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
147        loop {
148            let this = &mut *self;
149            match &mut this.state {
150                State::Live(file) => {
151                    let date = Utc::now().date_naive();
152                    if file.len < MAX_LOG_SIZE && date <= file.date {
153                        return file.poll_ready(cx);
154                    }
155
156                    ready!(file.poll_close(cx))?;
157
158                    let number = this.next_archive_index;
159                    if date > file.date {
160                        this.next_archive_index = 0;
161                    } else {
162                        this.next_archive_index += 1;
163                    }
164
165                    this.state = State::Rotating(Box::pin(rotate(
166                        log_dir(),
167                        this.name,
168                        file.date,
169                        number,
170                        this.max_archive_size,
171                        this.max_archive_days,
172                        this.archive_locator.clone(),
173                    )));
174                }
175                State::Rotating(future) => match ready!(future.as_mut().poll(cx)) {
176                    Ok(file) => {
177                        self.state = State::Live(CurrentFile {
178                            sink: BufBytesSink::new(FileBytesSink::new(file)),
179                            len: 0,
180                            date: Utc::now().date_naive(),
181                        });
182                    }
183                    Err(e) => {
184                        let path = log_path(log_dir(), this.name);
185                        this.state =
186                            State::Rotating(Box::pin(async move { open_log(&path).await }));
187                        return Poll::Ready(Err(e));
188                    }
189                },
190            }
191        }
192    }
193
194    fn start_send(mut self: Pin<&mut Self>, item: Bytes) -> Result<(), Self::Error> {
195        match &mut self.state {
196            State::Live(file) => file.start_send(item),
197            State::Rotating(_) => panic!("start_send called without poll_ready"),
198        }
199    }
200
201    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
202        match &mut self.state {
203            State::Live(file) => file.poll_flush(cx),
204            State::Rotating(_) => Poll::Ready(Ok(())),
205        }
206    }
207
208    fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
209        match &mut self.state {
210            State::Live(file) => file.poll_close(cx),
211            State::Rotating(_) => Poll::Ready(Ok(())),
212        }
213    }
214}
215
216async fn open_log(path: &Path) -> io::Result<File> {
217    OpenOptions::new()
218        .write(true)
219        .append(true)
220        .create(true)
221        .open(path)
222        .await
223}
224
225fn log_dir() -> &'static Path {
226    Path::new("var/log")
227}
228
229fn log_path(dir: &Path, name: &str) -> PathBuf {
230    let mut path = dir.to_path_buf();
231    path.push(format!("{name}.log"));
232    path
233}
234
235fn archive_path(dir: &Path, name: &str, date: NaiveDate, number: u32) -> PathBuf {
236    let mut path = dir.to_path_buf();
237    path.push(format!("{name}-{date}-{number}.log"));
238    path
239}
240
241fn archive_gz_tmp_path(dir: &Path, name: &str, date: NaiveDate, number: u32) -> PathBuf {
242    let mut path = dir.to_path_buf();
243    path.push(format!("{name}-{date}-{number}.log.gz.tmp"));
244    path
245}
246
247fn archive_gz_path(dir: &Path, name: &str, date: NaiveDate, number: u32) -> PathBuf {
248    let mut path = dir.to_path_buf();
249    path.push(format!("{name}-{date}-{number}.log.gz"));
250    path
251}
252
253async fn clear_old_archives(
254    dir: &Path,
255    date: NaiveDate,
256    max_archive_size: u64,
257    max_archive_days: u32,
258    archive_locator: &ArchiveLocator,
259) -> io::Result<()> {
260    let logs = archive_locator.archived_logs(dir).await?;
261    clear_old_archives_inner(date, max_archive_size, max_archive_days, logs).await
262}
263
264// split out for testing
265async fn clear_old_archives_inner(
266    date: NaiveDate,
267    max_archive_size: u64,
268    max_archive_days: u32,
269    mut logs: Vec<ArchivedLog>,
270) -> io::Result<()> {
271    logs.sort_by_key(|l| (l.date, l.number));
272
273    let mut total_size = logs.iter().map(|l| l.len).sum::<u64>();
274
275    let mut date_cutoff = date;
276    // do a silly loop to make sure we're correct WRT leap things
277    for _ in 0..max_archive_days {
278        date_cutoff = date_cutoff.pred_opt().unwrap();
279    }
280
281    for log in logs {
282        if log.date >= date_cutoff && total_size < max_archive_size {
283            break;
284        }
285
286        // management infrastructure could be cleaning these up concurrently, so an error is ok
287        let _ = fs::remove_file(&log.path).await;
288        total_size -= log.len;
289    }
290
291    Ok(())
292}
293
294async fn clear_tmp_files(dir: &Path, archive_locator: &ArchiveLocator) -> io::Result<()> {
295    for log in archive_locator.tmp_files(dir).await? {
296        fs::remove_file(&log.path).await?;
297    }
298
299    Ok(())
300}
301
302async fn restart_compression(
303    dir: &Path,
304    name: &str,
305    archive_locator: &ArchiveLocator,
306) -> io::Result<()> {
307    for log in archive_locator.uncompressed_logs(dir).await? {
308        let dir = dir.to_path_buf();
309        let name = name.to_string();
310        task::spawn(async move {
311            let _ = compress(&dir, &name, log.date, log.number).await;
312        });
313    }
314
315    Ok(())
316}
317
318async fn compress(dir: &Path, name: &str, date: NaiveDate, number: u32) -> io::Result<()> {
319    let source_path = archive_path(dir, name, date, number);
320    let mut source = File::open(&source_path).await?;
321
322    let tmp_path = archive_gz_tmp_path(dir, name, date, number);
323    let target = File::create(&tmp_path).await?;
324    let mut target = GzipEncoder::new(target);
325
326    io::copy(&mut source, &mut target).await?;
327    target.shutdown().await?;
328
329    let path = archive_gz_path(dir, name, date, number);
330    fs::rename(&tmp_path, &path).await?;
331
332    fs::remove_file(&source_path).await?;
333
334    Ok(())
335}
336
337async fn rotate(
338    dir: &Path,
339    name: &'static str,
340    date: NaiveDate,
341    number: u32,
342    max_archive_size: u64,
343    max_archive_days: u32,
344    archive_locator: Arc<ArchiveLocator>,
345) -> io::Result<File> {
346    let log_path = log_path(dir, name);
347    let tmp_path = archive_path(dir, name, date, number);
348
349    fs::rename(&log_path, &tmp_path).await?;
350
351    let dir = dir.to_path_buf();
352    task::spawn(async move {
353        let _ = compress(&dir, name, date, number).await;
354        // clear archives based on the current date rather than the date of the log being archived.
355        let _ = clear_old_archives(
356            &dir,
357            Utc::now().date_naive(),
358            max_archive_size,
359            max_archive_days,
360            &archive_locator,
361        )
362        .await;
363    });
364
365    open_log(&log_path).await
366}
367
368struct ArchiveLocator {
369    gz_regex: Regex,
370    gz_tmp_regex: Regex,
371    raw_regex: Regex,
372}
373
374impl ArchiveLocator {
375    fn new(name: &str) -> ArchiveLocator {
376        let gz_regex = format!(
377            r"^{}-(\d{{4}})-(\d{{2}})-(\d{{2}})-(\d+)\.log\.gz$",
378            regex::escape(name)
379        );
380        let gz_tmp_regex = format!(
381            r"^{}-(\d{{4}})-(\d{{2}})-(\d{{2}})-(\d+)\.log\.gz\.tmp$",
382            regex::escape(name)
383        );
384        let raw_regex = format!(
385            r"^{}-(\d{{4}})-(\d{{2}})-(\d{{2}})-(\d+)\.log$",
386            regex::escape(name)
387        );
388        ArchiveLocator {
389            gz_regex: Regex::new(&gz_regex).unwrap(),
390            gz_tmp_regex: Regex::new(&gz_tmp_regex).unwrap(),
391            raw_regex: Regex::new(&raw_regex).unwrap(),
392        }
393    }
394
395    async fn uncompressed_logs(&self, dir: &Path) -> io::Result<Vec<ArchivedLog>> {
396        self.get_logs(&self.raw_regex, dir).await
397    }
398
399    async fn tmp_files(&self, dir: &Path) -> io::Result<Vec<ArchivedLog>> {
400        self.get_logs(&self.gz_tmp_regex, dir).await
401    }
402
403    async fn archived_logs(&self, dir: &Path) -> io::Result<Vec<ArchivedLog>> {
404        self.get_logs(&self.gz_regex, dir).await
405    }
406
407    async fn get_logs(&self, regex: &Regex, dir: &Path) -> io::Result<Vec<ArchivedLog>> {
408        let mut logs = vec![];
409        let mut files = fs::read_dir(dir).await?;
410        while let Some(file) = files.next_entry().await? {
411            let name = file.file_name();
412            let name = match name.to_str() {
413                Some(name) => name,
414                None => continue,
415            };
416
417            let captures = match regex.captures(name) {
418                Some(captures) => captures,
419                None => continue,
420            };
421
422            let year = captures[1].parse().unwrap();
423            let month = captures[2].parse().unwrap();
424            let day = captures[3].parse().unwrap();
425            let date = match NaiveDate::from_ymd_opt(year, month, day) {
426                Some(date) => date,
427                None => continue,
428            };
429
430            let number = match captures[4].parse() {
431                Ok(number) => number,
432                Err(_) => continue,
433            };
434
435            let len = file.metadata().await?.len();
436
437            let log = ArchivedLog {
438                path: file.path(),
439                date,
440                number,
441                len,
442            };
443            logs.push(log);
444        }
445        Ok(logs)
446    }
447}
448
449#[derive(Debug, PartialEq)]
450struct ArchivedLog {
451    path: PathBuf,
452    date: NaiveDate,
453    number: u32,
454    len: u64,
455}
456
457#[pin_project]
458struct FileBytesSink {
459    #[pin]
460    file: File,
461    pending: Bytes,
462}
463
464impl FileBytesSink {
465    fn new(file: File) -> Self {
466        FileBytesSink {
467            file,
468            pending: Bytes::new(),
469        }
470    }
471}
472
473impl Sink<Bytes> for FileBytesSink {
474    type Error = io::Error;
475
476    fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
477        self.poll_flush(cx)
478    }
479
480    fn start_send(self: Pin<&mut Self>, item: Bytes) -> Result<(), Self::Error> {
481        let this = self.project();
482        debug_assert!(this.pending.is_empty());
483        *this.pending = item;
484
485        Ok(())
486    }
487
488    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
489        let mut this = self.project();
490
491        while !this.pending.is_empty() {
492            let nwritten = ready!(this.file.as_mut().poll_write(cx, this.pending))?;
493            this.pending.advance(nwritten);
494        }
495        ready!(this.file.poll_flush(cx))?;
496
497        Poll::Ready(Ok(()))
498    }
499
500    fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
501        self.poll_flush(cx)
502    }
503}
504
505#[cfg(test)]
506mod test {
507    use super::*;
508    use async_compression::tokio::bufread::GzipDecoder;
509    use tokio::io::AsyncReadExt;
510
511    #[test]
512    fn log_path_format() {
513        let name = "service";
514
515        assert_eq!(log_path(log_dir(), name), Path::new("var/log/service.log"));
516    }
517
518    #[test]
519    fn archive_tmp_path_format() {
520        let name = "service";
521        let date = NaiveDate::from_ymd_opt(2017, 4, 20).unwrap();
522        let number = 3;
523
524        assert_eq!(
525            archive_path(log_dir(), name, date, number),
526            Path::new("var/log/service-2017-04-20-3.log"),
527        );
528    }
529
530    #[test]
531    fn archive_path_format() {
532        let name = "service";
533        let date = NaiveDate::from_ymd_opt(2017, 4, 20).unwrap();
534        let number = 3;
535
536        assert_eq!(
537            archive_gz_path(log_dir(), name, date, number),
538            Path::new("var/log/service-2017-04-20-3.log.gz"),
539        );
540    }
541
542    #[tokio::test]
543    async fn compress_validity() {
544        let dir = tempfile::tempdir().unwrap();
545
546        let name = "service";
547        let date = NaiveDate::from_ymd_opt(2017, 4, 20).unwrap();
548        let number = 3;
549
550        let tmp_path = archive_path(dir.path(), name, date, number);
551        let mut file = open_log(&tmp_path).await.unwrap();
552        file.write_all(b"hello world").await.unwrap();
553        file.flush().await.unwrap();
554
555        compress(dir.path(), name, date, number).await.unwrap();
556
557        let archive_path = archive_gz_path(dir.path(), name, date, number);
558        let file = fs::read(&archive_path).await.unwrap();
559        let mut buf = vec![];
560        GzipDecoder::new(&mut &file[..])
561            .read_to_end(&mut buf)
562            .await
563            .unwrap();
564
565        assert_eq!(buf, b"hello world");
566    }
567
568    #[tokio::test]
569    async fn archive_locator() {
570        let dir = tempfile::tempdir().unwrap();
571
572        let service_path = log_path(dir.path(), "service");
573        let file = File::create(&service_path).await.unwrap();
574        file.set_len(1).await.unwrap();
575
576        let requests_path = log_path(dir.path(), "requests");
577        let file = File::create(&requests_path).await.unwrap();
578        file.set_len(2).await.unwrap();
579
580        let day1 = NaiveDate::from_ymd_opt(2017, 4, 20).unwrap();
581        let service_archive_1_0_path = archive_gz_path(dir.path(), "service", day1, 0);
582        let file = File::create(&service_archive_1_0_path).await.unwrap();
583        file.set_len(3).await.unwrap();
584
585        let service_archive_1_1_path = archive_gz_path(dir.path(), "service", day1, 1);
586        let file = File::create(&service_archive_1_1_path).await.unwrap();
587        file.set_len(4).await.unwrap();
588
589        let day2 = NaiveDate::from_ymd_opt(2017, 4, 21).unwrap();
590        let service_archive_2_0_path = archive_gz_path(dir.path(), "service", day2, 0);
591        let file = File::create(&service_archive_2_0_path).await.unwrap();
592        file.set_len(5).await.unwrap();
593
594        let service_archive_2_1_path = archive_gz_path(dir.path(), "service", day2, 1);
595        let file = File::create(&service_archive_2_1_path).await.unwrap();
596        file.set_len(6).await.unwrap();
597
598        let requests_archive_1_0_path = archive_gz_path(dir.path(), "requests", day1, 0);
599        let file = File::create(&requests_archive_1_0_path).await.unwrap();
600        file.set_len(7).await.unwrap();
601
602        let locator = ArchiveLocator::new("service");
603
604        let mut logs = locator.archived_logs(dir.path()).await.unwrap();
605        logs.sort_by_key(|l| (l.date, l.number));
606
607        let expected = [
608            ArchivedLog {
609                path: service_archive_1_0_path,
610                date: day1,
611                number: 0,
612                len: 3,
613            },
614            ArchivedLog {
615                path: service_archive_1_1_path,
616                date: day1,
617                number: 1,
618                len: 4,
619            },
620            ArchivedLog {
621                path: service_archive_2_0_path,
622                date: day2,
623                number: 0,
624                len: 5,
625            },
626            ArchivedLog {
627                path: service_archive_2_1_path,
628                date: day2,
629                number: 1,
630                len: 6,
631            },
632        ];
633
634        assert_eq!(logs, expected);
635    }
636
637    #[tokio::test]
638    async fn clear_old_archives_always_deletes_old_logs() {
639        let dir = tempfile::tempdir().unwrap();
640
641        let day1 = NaiveDate::from_ymd_opt(2017, 4, 20).unwrap();
642        let service_archive_1_0_path = archive_gz_path(dir.path(), "service", day1, 0);
643        File::create(&service_archive_1_0_path).await.unwrap();
644
645        let service_archive_1_1_path = archive_gz_path(dir.path(), "service", day1, 1);
646        File::create(&service_archive_1_1_path).await.unwrap();
647
648        let day2 = NaiveDate::from_ymd_opt(2017, 4, 21).unwrap();
649        let service_archive_2_0_path = archive_gz_path(dir.path(), "service", day2, 0);
650        File::create(&service_archive_2_0_path).await.unwrap();
651
652        let service_archive_2_1_tmp_path = archive_path(dir.path(), "service", day2, 1);
653        File::create(&service_archive_2_1_tmp_path).await.unwrap();
654
655        let logs = vec![
656            ArchivedLog {
657                path: service_archive_1_0_path.clone(),
658                date: day1,
659                number: 0,
660                len: 0,
661            },
662            ArchivedLog {
663                path: service_archive_1_1_path.clone(),
664                date: day1,
665                number: 1,
666                len: 0,
667            },
668            ArchivedLog {
669                path: service_archive_2_0_path.clone(),
670                date: day2,
671                number: 0,
672                len: 0,
673            },
674            ArchivedLog {
675                path: service_archive_2_1_tmp_path.clone(),
676                date: day2,
677                number: 1,
678                len: 0,
679            },
680        ];
681
682        let date = NaiveDate::from_ymd_opt(2017, 5, 21).unwrap();
683        clear_old_archives_inner(date, 1024 * 1024 * 1024, 30, logs)
684            .await
685            .unwrap();
686
687        assert!(!service_archive_1_0_path.exists());
688        assert!(!service_archive_1_1_path.exists());
689        assert!(service_archive_2_0_path.exists());
690        assert!(service_archive_2_1_tmp_path.exists());
691    }
692
693    #[tokio::test]
694    async fn clear_old_archives_deletes_to_save_space() {
695        let dir = tempfile::tempdir().unwrap();
696
697        let day1 = NaiveDate::from_ymd_opt(2017, 4, 20).unwrap();
698        let service_archive_1_0_path = archive_gz_path(dir.path(), "service", day1, 0);
699        File::create(&service_archive_1_0_path).await.unwrap();
700
701        let service_archive_1_1_path = archive_gz_path(dir.path(), "service", day1, 1);
702        File::create(&service_archive_1_1_path).await.unwrap();
703
704        let day2 = NaiveDate::from_ymd_opt(2017, 4, 21).unwrap();
705        let service_archive_2_0_path = archive_gz_path(dir.path(), "service", day2, 0);
706        File::create(&service_archive_2_0_path).await.unwrap();
707
708        let service_archive_2_1_tmp_path = archive_gz_tmp_path(dir.path(), "service", day2, 1);
709        File::create(&service_archive_2_1_tmp_path).await.unwrap();
710
711        let logs = vec![
712            ArchivedLog {
713                path: service_archive_1_0_path.clone(),
714                date: day1,
715                number: 0,
716                len: 4,
717            },
718            ArchivedLog {
719                path: service_archive_1_1_path.clone(),
720                date: day1,
721                number: 1,
722                len: 5,
723            },
724            ArchivedLog {
725                path: service_archive_2_0_path.clone(),
726                date: day2,
727                number: 0,
728                len: 1,
729            },
730            ArchivedLog {
731                path: service_archive_2_1_tmp_path.clone(),
732                date: day2,
733                number: 1,
734                len: 1023,
735            },
736        ];
737
738        let date = NaiveDate::from_ymd_opt(2017, 4, 21).unwrap();
739        clear_old_archives_inner(date, 1024, 30, logs)
740            .await
741            .unwrap();
742
743        assert!(!service_archive_1_0_path.exists());
744        assert!(!service_archive_1_1_path.exists());
745        assert!(!service_archive_2_0_path.exists());
746        assert!(service_archive_2_1_tmp_path.exists());
747    }
748
749    #[tokio::test]
750    async fn clear_old_archives_ignores_missing_files() {
751        let dir = tempfile::tempdir().unwrap();
752
753        let day1 = NaiveDate::from_ymd_opt(2017, 4, 20).unwrap();
754        let service_archive_1_0_path = archive_gz_path(dir.path(), "service", day1, 0);
755        // not actually making this
756
757        let service_archive_1_1_path = archive_gz_path(dir.path(), "service", day1, 1);
758        File::create(&service_archive_1_1_path).await.unwrap();
759
760        let day2 = NaiveDate::from_ymd_opt(2017, 4, 21).unwrap();
761        let service_archive_2_0_path = archive_gz_path(dir.path(), "service", day2, 0);
762        File::create(&service_archive_2_0_path).await.unwrap();
763
764        let service_archive_2_1_tmp_path = archive_gz_tmp_path(dir.path(), "service", day2, 1);
765        File::create(&service_archive_2_1_tmp_path).await.unwrap();
766
767        let logs = vec![
768            ArchivedLog {
769                path: service_archive_1_0_path.clone(),
770                date: day1,
771                number: 0,
772                len: 4,
773            },
774            ArchivedLog {
775                path: service_archive_1_1_path.clone(),
776                date: day1,
777                number: 1,
778                len: 5,
779            },
780            ArchivedLog {
781                path: service_archive_2_0_path.clone(),
782                date: day2,
783                number: 0,
784                len: 1,
785            },
786            ArchivedLog {
787                path: service_archive_2_1_tmp_path.clone(),
788                date: day2,
789                number: 1,
790                len: 1023,
791            },
792        ];
793
794        let date = NaiveDate::from_ymd_opt(2017, 4, 21).unwrap();
795        clear_old_archives_inner(date, 1024, 30, logs)
796            .await
797            .unwrap();
798
799        assert!(!service_archive_1_0_path.exists());
800        assert!(!service_archive_1_1_path.exists());
801        assert!(!service_archive_2_0_path.exists());
802        assert!(service_archive_2_1_tmp_path.exists());
803    }
804}