1use std::ffi::OsString;
4use std::fs::{self, File, OpenOptions};
5use std::io::{self, BufWriter, Write};
6use std::path::{Path, PathBuf};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14#[non_exhaustive]
15pub enum Rotation {
16 #[default]
19 Never,
20 Minutely,
23 Hourly,
25 Daily,
27}
28
29impl Rotation {
30 const fn period_secs(self) -> Option<u64> {
31 match self {
32 Self::Never => None,
33 Self::Minutely => Some(60),
34 Self::Hourly => Some(3_600),
35 Self::Daily => Some(86_400),
36 }
37 }
38}
39
40#[cfg_attr(all(feature = "json"), doc = "```no_run")]
95#[cfg_attr(not(all(feature = "json")), doc = "```ignore")]
96pub struct RotatingFile {
109 path: PathBuf,
110 file: Option<BufWriter<File>>,
111 size: u64,
112 max_bytes: Option<u64>,
113 rotation: Rotation,
114 max_files: usize,
115 buffer_capacity: usize,
116 period: Option<u64>,
118 deferred_error: Option<io::Error>,
120 now: fn() -> SystemTime,
121}
122
123impl RotatingFile {
124 pub fn builder(path: impl AsRef<Path>) -> RotatingFileBuilder {
127 RotatingFileBuilder {
128 path: path.as_ref().to_path_buf(),
129 max_bytes: None,
130 rotation: Rotation::Never,
131 max_files: 5,
132 buffer_capacity: 8 * 1024,
133 }
134 }
135
136 #[must_use]
138 pub fn path(&self) -> &Path {
139 &self.path
140 }
141
142 #[must_use]
145 pub fn current_size(&self) -> u64 {
146 self.size
147 }
148
149 pub fn rotate_now(&mut self) -> io::Result<()> {
156 self.rotate()
157 }
158
159 fn rotated_path(&self, index: usize) -> PathBuf {
160 let mut name = OsString::from(self.path.as_os_str());
161 name.push(format!(".{index}"));
162 PathBuf::from(name)
163 }
164
165 fn current_period(&self) -> Option<u64> {
166 self.rotation
169 .period_secs()
170 .map(|p| unix_secs((self.now)()) / p)
171 }
172
173 fn should_rotate(&mut self, incoming: usize) -> bool {
174 if self.size == 0 {
175 self.period = self.current_period();
177 return false;
178 }
179 let too_big = self
180 .max_bytes
181 .is_some_and(|max| self.size.saturating_add(incoming as u64) > max);
182 too_big
183 || self
184 .period
185 .is_some_and(|open| self.current_period() != Some(open))
186 }
187
188 fn rotate(&mut self) -> io::Result<()> {
189 if let Some(mut writer) = self.file.take() {
190 if let Err(e) = writer.flush() {
191 self.file = Some(writer);
192 return Err(e);
193 }
194 drop(writer);
197 }
198 let rotated = self.move_active_into_history();
199 let reopened = open_append(&self.path, self.buffer_capacity);
200 match reopened {
201 Ok((writer, size)) => {
202 self.file = Some(writer);
203 self.size = if rotated.is_ok() { size } else { 0 };
208 self.period = self.current_period();
209 rotated
210 }
211 Err(e) => Err(e),
212 }
213 }
214
215 fn sibling(&self, suffix: &str) -> PathBuf {
216 let mut name = OsString::from(self.path.as_os_str());
217 name.push(suffix);
218 PathBuf::from(name)
219 }
220
221 fn move_active_into_history(&self) -> io::Result<()> {
233 if self.max_files == 0 {
234 return remove_if_exists(&self.path);
236 }
237 let staging = self.sibling(STAGING_SUFFIX);
238 let pruning = self.sibling(PRUNING_SUFFIX);
239 if fs::symlink_metadata(&staging).is_ok() {
240 return Err(io::Error::new(
243 io::ErrorKind::AlreadyExists,
244 format!(
245 "{} was left by an interrupted rotation and holds log records; move it aside to resume rotation",
246 staging.display()
247 ),
248 ));
249 }
250 remove_if_exists(&pruning)?;
253
254 let mut moves = Vec::with_capacity(self.max_files + 2);
255 moves.push(Move::required(self.path.clone(), staging.clone()));
256 moves.push(Move::optional(
257 self.rotated_path(self.max_files),
258 pruning.clone(),
259 ));
260 for index in (1..self.max_files).rev() {
261 moves.push(Move::optional(
262 self.rotated_path(index),
263 self.rotated_path(index + 1),
264 ));
265 }
266 moves.push(Move::required(staging, self.rotated_path(1)));
267
268 let mut done = Vec::with_capacity(moves.len());
269 for step in &moves {
270 match rename_file(&step.from, &step.to) {
271 Ok(()) => done.push(step),
272 Err(e) if !step.required && e.kind() == io::ErrorKind::NotFound => {}
273 Err(e) => {
274 roll_back(&done);
275 return Err(e);
276 }
277 }
278 }
279 remove_if_exists(&pruning)
282 }
283}
284
285const STAGING_SUFFIX: &str = ".rotating";
287const PRUNING_SUFFIX: &str = ".pruning";
290
291struct Move {
293 from: PathBuf,
294 to: PathBuf,
295 required: bool,
297}
298
299impl Move {
300 const fn required(from: PathBuf, to: PathBuf) -> Self {
301 Self {
302 from,
303 to,
304 required: true,
305 }
306 }
307
308 const fn optional(from: PathBuf, to: PathBuf) -> Self {
309 Self {
310 from,
311 to,
312 required: false,
313 }
314 }
315}
316
317fn roll_back(done: &[&Move]) {
326 for step in done.iter().rev() {
327 if rename_file(&step.to, &step.from).is_err() {
328 return;
329 }
330 }
331}
332
333impl Write for RotatingFile {
334 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
335 if self.should_rotate(buf.len()) {
336 if let Err(e) = self.rotate() {
337 if self.deferred_error.is_none() {
339 self.deferred_error = Some(e);
340 }
341 }
342 }
343 let writer = match self.file.as_mut() {
344 Some(w) => w,
345 None => {
346 let (writer, size) = open_append(&self.path, self.buffer_capacity)?;
347 self.size = size;
348 self.file.insert(writer)
349 }
350 };
351 writer.write_all(buf)?;
352 self.size = self.size.saturating_add(buf.len() as u64);
353 Ok(buf.len())
354 }
355
356 fn flush(&mut self) -> io::Result<()> {
357 if let Some(writer) = self.file.as_mut() {
358 writer.flush()?;
359 }
360 match self.deferred_error.take() {
361 Some(e) => Err(e),
362 None => Ok(()),
363 }
364 }
365}
366
367impl Drop for RotatingFile {
368 fn drop(&mut self) {
369 if let Some(writer) = self.file.as_mut() {
370 let _flush_in_drop = writer.flush();
373 }
374 }
375}
376
377impl core::fmt::Debug for RotatingFile {
378 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
379 f.debug_struct("RotatingFile")
380 .field("path", &self.path)
381 .field("size", &self.size)
382 .field("max_bytes", &self.max_bytes)
383 .field("rotation", &self.rotation)
384 .field("max_files", &self.max_files)
385 .finish_non_exhaustive()
386 }
387}
388
389#[derive(Debug, Clone)]
391#[must_use = "call `open()` to create the writer"]
392pub struct RotatingFileBuilder {
393 path: PathBuf,
394 max_bytes: Option<u64>,
395 rotation: Rotation,
396 max_files: usize,
397 buffer_capacity: usize,
398}
399
400impl RotatingFileBuilder {
401 pub const fn max_bytes(mut self, bytes: u64) -> Self {
403 self.max_bytes = Some(bytes);
404 self
405 }
406
407 pub const fn rotation(mut self, rotation: Rotation) -> Self {
409 self.rotation = rotation;
410 self
411 }
412
413 pub const fn max_files(mut self, count: usize) -> Self {
416 self.max_files = count;
417 self
418 }
419
420 pub const fn buffer_capacity(mut self, bytes: usize) -> Self {
422 self.buffer_capacity = bytes;
423 self
424 }
425
426 pub fn open(self) -> io::Result<RotatingFile> {
435 self.open_with_clock(SystemTime::now)
436 }
437
438 fn open_with_clock(self, now: fn() -> SystemTime) -> io::Result<RotatingFile> {
439 let (writer, size) = open_append(&self.path, self.buffer_capacity)?;
440 let period = self.rotation.period_secs().map(|p| {
441 let started = if size > 0 {
442 writer
443 .get_ref()
444 .metadata()
445 .and_then(|m| m.modified())
446 .unwrap_or_else(|_| now())
447 } else {
448 now()
449 };
450 unix_secs(started) / p
451 });
452 Ok(RotatingFile {
453 path: self.path,
454 file: Some(writer),
455 size,
456 max_bytes: self.max_bytes,
457 rotation: self.rotation,
458 max_files: self.max_files,
459 buffer_capacity: self.buffer_capacity,
460 period,
461 deferred_error: None,
462 now,
463 })
464 }
465}
466
467fn open_append(path: &Path, capacity: usize) -> io::Result<(BufWriter<File>, u64)> {
468 let file = OpenOptions::new().create(true).append(true).open(path)?;
469 let size = file.metadata()?.len();
470 Ok((BufWriter::with_capacity(capacity, file), size))
471}
472
473fn unix_secs(t: SystemTime) -> u64 {
474 t.duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs())
475}
476
477fn remove_if_exists(path: &Path) -> io::Result<()> {
478 match fs::remove_file(path) {
479 Err(e) if e.kind() != io::ErrorKind::NotFound => Err(e),
480 _ => Ok(()),
481 }
482}
483
484fn rename_file(from: &Path, to: &Path) -> io::Result<()> {
485 #[cfg(test)]
486 if tests::rename_is_blocked(from, to) {
487 return Err(io::Error::new(
488 io::ErrorKind::PermissionDenied,
489 "rename blocked by the test",
490 ));
491 }
492 fs::rename(from, to)
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use std::sync::atomic::{AtomicU64, Ordering};
499
500 struct TempDir(PathBuf);
501
502 impl TempDir {
503 fn new(name: &str) -> Self {
504 let dir =
505 std::env::temp_dir().join(format!("log-io-rotate-{}-{name}", std::process::id()));
506 let _ = fs::remove_dir_all(&dir);
507 fs::create_dir_all(&dir).unwrap();
508 Self(dir)
509 }
510 fn file(&self, name: &str) -> PathBuf {
511 self.0.join(name)
512 }
513 }
514
515 impl Drop for TempDir {
516 fn drop(&mut self) {
517 let _ = fs::remove_dir_all(&self.0);
518 }
519 }
520
521 fn read(path: &Path) -> String {
522 fs::read_to_string(path).unwrap_or_default()
523 }
524
525 #[test]
526 fn test_rotating_file_rotates_by_size_and_keeps_max_files() {
527 let dir = TempDir::new("size");
528 let path = dir.file("app.log");
529 let mut w = RotatingFile::builder(&path)
530 .max_bytes(10)
531 .max_files(2)
532 .open()
533 .unwrap();
534 for record in ["aaaa1\n", "bbbb2\n", "cccc3\n", "dddd4\n"] {
535 w.write_all(record.as_bytes()).unwrap();
536 }
537 w.flush().unwrap();
538 assert_eq!(read(&path), "dddd4\n");
539 assert_eq!(read(&dir.file("app.log.1")), "cccc3\n");
540 assert_eq!(read(&dir.file("app.log.2")), "bbbb2\n");
541 assert!(
542 !dir.file("app.log.3").exists(),
543 "max_files must be enforced"
544 );
545 }
546
547 #[test]
548 fn test_rotating_file_never_splits_a_record() {
549 let dir = TempDir::new("whole");
550 let path = dir.file("a.log");
551 let mut w = RotatingFile::builder(&path).max_bytes(8).open().unwrap();
552 w.write_all(b"0123456789abcdef\n").unwrap();
553 w.write_all(b"xy\n").unwrap();
554 w.flush().unwrap();
555 assert_eq!(read(&dir.file("a.log.1")), "0123456789abcdef\n");
556 assert_eq!(read(&path), "xy\n");
557 assert_eq!(w.current_size(), 3);
558 }
559
560 #[test]
561 fn test_rotating_file_max_files_zero_discards_history() {
562 let dir = TempDir::new("zero");
563 let path = dir.file("z.log");
564 let mut w = RotatingFile::builder(&path)
565 .max_bytes(4)
566 .max_files(0)
567 .open()
568 .unwrap();
569 w.write_all(b"one\n").unwrap();
570 w.write_all(b"two\n").unwrap();
571 w.flush().unwrap();
572 assert_eq!(read(&path), "two\n");
573 assert!(!dir.file("z.log.1").exists());
574 }
575
576 #[test]
577 fn test_rotating_file_appends_to_existing_and_counts_its_size() {
578 let dir = TempDir::new("existing");
579 let path = dir.file("e.log");
580 fs::write(&path, "12345").unwrap();
581 let mut w = RotatingFile::builder(&path).max_bytes(8).open().unwrap();
582 assert_eq!(w.current_size(), 5);
583 w.write_all(b"6789\n").unwrap();
584 w.flush().unwrap();
585 assert_eq!(read(&dir.file("e.log.1")), "12345");
586 assert_eq!(read(&path), "6789\n");
587 }
588
589 static FAKE_NOW: AtomicU64 = AtomicU64::new(0);
590
591 fn fake_clock() -> SystemTime {
592 UNIX_EPOCH + std::time::Duration::from_secs(FAKE_NOW.load(Ordering::SeqCst))
593 }
594
595 #[test]
596 fn test_rotating_file_rotates_on_period_boundary() {
597 let dir = TempDir::new("time");
598 let path = dir.file("t.log");
599 FAKE_NOW.store(86_400 * 10 + 5, Ordering::SeqCst);
600 let mut w = RotatingFile::builder(&path)
601 .rotation(Rotation::Daily)
602 .open_with_clock(fake_clock)
603 .unwrap();
604 w.write_all(b"day10\n").unwrap();
605 FAKE_NOW.store(86_400 * 10 + 80_000, Ordering::SeqCst);
606 w.write_all(b"day10b\n").unwrap();
607 FAKE_NOW.store(86_400 * 11 + 1, Ordering::SeqCst);
608 w.write_all(b"day11\n").unwrap();
609 w.flush().unwrap();
610 assert_eq!(read(&dir.file("t.log.1")), "day10\nday10b\n");
611 assert_eq!(read(&path), "day11\n");
612 }
613
614 #[test]
615 fn test_rotating_file_rotate_now_and_accessors() {
616 let dir = TempDir::new("manual");
617 let path = dir.file("m.log");
618 let mut w = RotatingFile::builder(&path)
619 .rotation(Rotation::Hourly)
620 .buffer_capacity(0)
621 .open()
622 .unwrap();
623 assert_eq!(w.path(), path.as_path());
624 w.write_all(b"before\n").unwrap();
625 w.rotate_now().unwrap();
626 w.write_all(b"after\n").unwrap();
627 w.flush().unwrap();
628 assert_eq!(read(&dir.file("m.log.1")), "before\n");
629 assert_eq!(read(&path), "after\n");
630 assert!(format!("{w:?}").contains("Hourly"));
631 assert_eq!(Rotation::default(), Rotation::Never);
632 assert_eq!(Rotation::Minutely.period_secs(), Some(60));
633 }
634
635 #[test]
636 fn test_rotating_file_prune_failure_is_reported_once_and_keeps_records() {
637 let dir = TempDir::new("fail");
638 let path = dir.file("f.log");
639 let mut w = RotatingFile::builder(&path)
640 .max_bytes(4)
641 .max_files(1)
642 .open()
643 .unwrap();
644 w.write_all(b"one\n").unwrap();
645 fs::create_dir(dir.file("f.log.1")).unwrap();
648 w.write_all(b"two\n").unwrap();
649 assert!(w.flush().is_err(), "rotation failure must surface on flush");
650 assert!(w.flush().is_ok(), "the error is reported once");
651 assert_eq!(read(&dir.file("f.log.1")), "one\n");
652 assert_eq!(read(&path), "two\n");
653 assert!(dir.file("f.log.pruning").is_dir());
654 w.write_all(b"three\n").unwrap();
657 assert!(w.flush().is_err());
658 assert_eq!(read(&dir.file("f.log.1")), "one\n");
659 assert_eq!(read(&path), "two\nthree\n", "no record may be lost");
660 }
661
662 #[test]
663 fn test_rotating_file_rotates_at_exactly_max_bytes_not_before() {
664 let dir = TempDir::new("edge");
665 let path = dir.file("edge.log");
666 let mut w = RotatingFile::builder(&path)
667 .max_bytes(10)
668 .buffer_capacity(0)
669 .open()
670 .unwrap();
671 w.write_all(b"01234").unwrap();
672 w.write_all(b"56789").unwrap();
673 assert!(!dir.file("edge.log.1").exists(), "exactly max_bytes fits");
674 w.write_all(b"a").unwrap();
675 assert_eq!(read(&dir.file("edge.log.1")), "0123456789");
676 assert_eq!(read(&path), "a");
677 }
678
679 #[test]
680 fn test_rotating_file_drop_flushes_buffered_records() {
681 let dir = TempDir::new("drop");
682 let path = dir.file("d.log");
683 let mut w = RotatingFile::builder(&path).open().unwrap();
684 w.write_all(b"buffered\n").unwrap();
685 assert_eq!(read(&path), "", "still in the write buffer");
686 drop(w);
687 assert_eq!(read(&path), "buffered\n");
688 }
689
690 #[test]
691 fn test_rotating_file_never_reads_the_clock_without_a_schedule() {
692 fn exploding_clock() -> SystemTime {
693 panic!("the clock must not be read for Rotation::Never");
694 }
695 let dir = TempDir::new("noclock");
696 let path = dir.file("n.log");
697 let mut w = RotatingFile::builder(&path)
698 .max_bytes(4)
699 .open_with_clock(exploding_clock)
700 .unwrap();
701 w.write_all(b"one\n").unwrap();
702 w.write_all(b"two\n").unwrap();
703 w.flush().unwrap();
704 assert_eq!(read(&dir.file("n.log.1")), "one\n");
705 }
706
707 thread_local! {
708 static BLOCKED: core::cell::RefCell<Vec<(PathBuf, PathBuf)>> =
712 const { core::cell::RefCell::new(Vec::new()) };
713 }
714
715 pub(super) fn rename_is_blocked(from: &Path, to: &Path) -> bool {
716 BLOCKED.with(|b| b.borrow().iter().any(|(f, t)| f == from && t == to))
717 }
718
719 fn block_rename(from: PathBuf, to: PathBuf) {
720 BLOCKED.with(|b| b.borrow_mut().push((from, to)));
721 }
722
723 fn unblock_renames() {
724 BLOCKED.with(|b| b.borrow_mut().clear());
725 }
726
727 fn seeded_writer(dir: &TempDir, base: &str) -> RotatingFile {
730 let mut w = RotatingFile::builder(dir.file(base))
731 .max_bytes(5)
732 .max_files(3)
733 .buffer_capacity(0)
734 .open()
735 .unwrap();
736 w.write_all(b"11111").unwrap();
737 seed_history(dir, base, 3);
738 w
739 }
740
741 fn rotation_files(dir: &TempDir) -> Vec<String> {
742 let mut names: Vec<String> = fs::read_dir(&dir.0)
743 .unwrap()
744 .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
745 .collect();
746 names.sort();
747 names
748 }
749
750 #[test]
751 fn test_rotating_file_blocked_active_rename_keeps_history_on_every_platform() {
752 let dir = TempDir::new("blocked-active");
753 let mut w = seeded_writer(&dir, "a.log");
754 block_rename(dir.file("a.log"), dir.file("a.log.rotating"));
755 for i in 0..6 {
756 w.write_all(format!("r{i}xxx").as_bytes()).unwrap();
757 let _reported = w.flush();
758 }
759 unblock_renames();
760 assert_history_intact(&dir, "a.log", 3);
761 assert_eq!(
762 read(&dir.file("a.log")),
763 "11111r0xxxr1xxxr2xxxr3xxxr4xxxr5xxx"
764 );
765 assert_eq!(
766 rotation_files(&dir),
767 ["a.log", "a.log.1", "a.log.2", "a.log.3"]
768 );
769 }
770
771 #[test]
772 fn test_rotating_file_blocked_history_rename_rolls_back() {
773 let dir = TempDir::new("blocked-history");
774 let mut w = seeded_writer(&dir, "h.log");
775 block_rename(dir.file("h.log.2"), dir.file("h.log.3"));
778 w.write_all(b"r0xxx").unwrap();
779 let err = w.flush().unwrap_err();
780 assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
781 unblock_renames();
782 assert_history_intact(&dir, "h.log", 3);
783 assert_eq!(read(&dir.file("h.log")), "11111r0xxx");
784 assert_eq!(
785 rotation_files(&dir),
786 ["h.log", "h.log.1", "h.log.2", "h.log.3"]
787 );
788 }
789
790 #[test]
791 fn test_rotating_file_blocked_final_rename_rolls_back() {
792 let dir = TempDir::new("blocked-final");
793 let mut w = seeded_writer(&dir, "s.log");
794 block_rename(dir.file("s.log.rotating"), dir.file("s.log.1"));
795 w.write_all(b"r0xxx").unwrap();
796 assert!(w.flush().is_err());
797 unblock_renames();
798 assert_history_intact(&dir, "s.log", 3);
799 assert_eq!(read(&dir.file("s.log")), "11111r0xxx");
800 w.rotate_now().unwrap();
802 w.write_all(b"r1xxx").unwrap();
803 w.flush().unwrap();
804 assert_eq!(read(&dir.file("s.log")), "r1xxx");
805 assert_eq!(read(&dir.file("s.log.1")), "11111r0xxx");
806 assert_eq!(read(&dir.file("s.log.2")), "OLD1\n");
807 assert_eq!(read(&dir.file("s.log.3")), "OLD2\n");
808 assert_eq!(
809 rotation_files(&dir),
810 ["s.log", "s.log.1", "s.log.2", "s.log.3"],
811 "OLD3 is pruned only by the rotation that completed"
812 );
813 }
814
815 #[test]
816 fn test_rotating_file_failed_rollback_deletes_nothing_and_stops_rotating() {
817 let dir = TempDir::new("blocked-rollback");
818 let mut w = seeded_writer(&dir, "r.log");
819 block_rename(dir.file("r.log.rotating"), dir.file("r.log.1"));
822 block_rename(dir.file("r.log.2"), dir.file("r.log.1"));
823 w.write_all(b"r0xxx").unwrap();
824 assert!(w.flush().is_err());
825 unblock_renames();
826 let mut contents: Vec<String> = rotation_files(&dir)
827 .iter()
828 .map(|name| read(&dir.file(name)))
829 .collect();
830 contents.sort();
831 assert_eq!(contents, ["11111", "OLD1\n", "OLD2\n", "OLD3\n", "r0xxx"]);
833 assert_eq!(read(&dir.file("r.log.rotating")), "11111");
834 w.write_all(b"r1xxx").unwrap();
837 w.write_all(b"r2xxx").unwrap();
838 let err = w.flush().unwrap_err();
839 assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
840 assert!(err.to_string().contains("r.log.rotating"), "{err}");
841 assert_eq!(read(&dir.file("r.log.rotating")), "11111");
842 assert_eq!(read(&dir.file("r.log")), "r0xxxr1xxxr2xxx");
843 }
844
845 fn seed_history(dir: &TempDir, base: &str, count: usize) {
846 for index in 1..=count {
847 fs::write(
848 dir.file(&format!("{base}.{index}")),
849 format!(
850 "OLD{index}
851"
852 ),
853 )
854 .unwrap();
855 }
856 }
857
858 fn assert_history_intact(dir: &TempDir, base: &str, count: usize) {
859 for index in 1..=count {
860 assert_eq!(
861 read(&dir.file(&format!("{base}.{index}"))),
862 format!(
863 "OLD{index}
864"
865 ),
866 "{base}.{index} must survive a failed rotation"
867 );
868 }
869 }
870
871 #[cfg(windows)]
875 #[test]
876 fn test_rotating_file_held_active_file_keeps_history_on_windows() {
877 use std::os::windows::fs::OpenOptionsExt;
878 const FILE_SHARE_READ: u32 = 0x1;
879 const FILE_SHARE_WRITE: u32 = 0x2;
880 let dir = TempDir::new("held");
881 let path = dir.file("held.log");
882 let mut w = RotatingFile::builder(&path)
883 .max_bytes(5)
884 .max_files(3)
885 .buffer_capacity(0)
886 .open()
887 .unwrap();
888 w.write_all(b"11111").unwrap();
889 seed_history(&dir, "held.log", 3);
890 let holder = OpenOptions::new()
891 .read(true)
892 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
893 .open(&path)
894 .unwrap();
895 let mut failures = 0;
896 for i in 0..6 {
897 w.write_all(format!("r{i}xxx").as_bytes()).unwrap();
898 if w.flush().is_err() {
899 failures += 1;
900 }
901 }
902 drop(holder);
903 assert!(failures > 0, "the blocked rotation must be reported");
904 assert_history_intact(&dir, "held.log", 3);
905 assert_eq!(read(&path), "11111r0xxxr1xxxr2xxxr3xxxr4xxxr5xxx");
906 assert!(!dir.file("held.log.4").exists());
907 w.rotate_now().unwrap();
909 assert_eq!(
910 read(&dir.file("held.log.1")),
911 "11111r0xxxr1xxxr2xxxr3xxxr4xxxr5xxx"
912 );
913 assert_eq!(
914 read(&dir.file("held.log.2")),
915 "OLD1
916"
917 );
918 assert_eq!(
919 read(&dir.file("held.log.3")),
920 "OLD2
921"
922 );
923 assert!(!dir.file("held.log.4").exists(), "max_files still applies");
924 }
925}