Skip to content

Commit 1dc94fa

Browse files
authored
appender: fix incorrect compare_exchange when rolling (#1989)
This branch fixes a bug in `RollingFileAppender` where `compare_exchange` is called with the _current timestamp_ as the "current value" of the next rollover timestamp, rather than the actual current value. This means that if the current time is *greater* than the rollover time, the CAS will fail and the appender will never roll over --- currently, rolling only functions correctly if we try to write to the file at *precisely* the rollover time. This means that, in practice, the appender almost never rolls over. I've fixed this by ensuring that the compare-and-swap is always performed with the current value of the atomic, rather than the current timestamp. `should_rollover` now returns an `Option` with the current value in it to indicate it's time to roll over, so that we perform the CAS with the value loaded in `should_rollover`. I've also added a test that exercises a file rollover using a mock time. This would have caught the bug described in #1987. Fixes #1987 Signed-off-by: Eliza Weisman <[email protected]>
1 parent c06d535 commit 1dc94fa

File tree

1 file changed

+158
-41
lines changed

1 file changed

+158
-41
lines changed

tracing-appender/src/rolling.rs

Lines changed: 158 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
//! ```
2929
use crate::sync::{RwLock, RwLockReadGuard};
3030
use std::{
31-
fmt::Debug,
31+
fmt::{self, Debug},
3232
fs::{self, File, OpenOptions},
3333
io::{self, Write},
3434
path::Path,
@@ -80,10 +80,11 @@ use time::{format_description, Duration, OffsetDateTime, Time};
8080
/// ```
8181
///
8282
/// [`MakeWriter`]: tracing_subscriber::fmt::writer::MakeWriter
83-
#[derive(Debug)]
8483
pub struct RollingFileAppender {
8584
state: Inner,
8685
writer: RwLock<File>,
86+
#[cfg(test)]
87+
now: Box<dyn Fn() -> OffsetDateTime + Send + Sync>,
8788
}
8889

8990
/// A [writer] that writes to a rolling log file.
@@ -135,36 +136,31 @@ impl RollingFileAppender {
135136
file_name_prefix: impl AsRef<Path>,
136137
) -> RollingFileAppender {
137138
let now = OffsetDateTime::now_utc();
138-
let log_directory = directory.as_ref().to_str().unwrap();
139-
let log_filename_prefix = file_name_prefix.as_ref().to_str().unwrap();
140-
141-
let filename = rotation.join_date(log_filename_prefix, &now);
142-
let next_date = rotation.next_date(&now);
143-
let writer = RwLock::new(
144-
create_writer(log_directory, &filename).expect("failed to create appender"),
145-
);
139+
let (state, writer) = Inner::new(now, rotation, directory, file_name_prefix);
146140
Self {
147-
state: Inner {
148-
log_directory: log_directory.to_string(),
149-
log_filename_prefix: log_filename_prefix.to_string(),
150-
next_date: AtomicUsize::new(
151-
next_date
152-
.map(|date| date.unix_timestamp() as usize)
153-
.unwrap_or(0),
154-
),
155-
rotation,
156-
},
141+
state,
157142
writer,
143+
#[cfg(test)]
144+
now: Box::new(OffsetDateTime::now_utc),
158145
}
159146
}
147+
148+
#[inline]
149+
fn now(&self) -> OffsetDateTime {
150+
#[cfg(test)]
151+
return (self.now)();
152+
153+
#[cfg(not(test))]
154+
OffsetDateTime::now_utc()
155+
}
160156
}
161157

162158
impl io::Write for RollingFileAppender {
163159
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
164-
let now = OffsetDateTime::now_utc();
160+
let now = self.now();
165161
let writer = self.writer.get_mut();
166-
if self.state.should_rollover(now) {
167-
let _did_cas = self.state.advance_date(now);
162+
if let Some(current_time) = self.state.should_rollover(now) {
163+
let _did_cas = self.state.advance_date(now, current_time);
168164
debug_assert!(_did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp...");
169165
self.state.refresh_writer(now, writer);
170166
}
@@ -179,20 +175,31 @@ impl io::Write for RollingFileAppender {
179175
impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for RollingFileAppender {
180176
type Writer = RollingWriter<'a>;
181177
fn make_writer(&'a self) -> Self::Writer {
182-
let now = OffsetDateTime::now_utc();
178+
let now = self.now();
183179

184180
// Should we try to roll over the log file?
185-
if self.state.should_rollover(now) {
181+
if let Some(current_time) = self.state.should_rollover(now) {
186182
// Did we get the right to lock the file? If not, another thread
187183
// did it and we can just make a writer.
188-
if self.state.advance_date(now) {
184+
if self.state.advance_date(now, current_time) {
189185
self.state.refresh_writer(now, &mut *self.writer.write());
190186
}
191187
}
192188
RollingWriter(self.writer.read())
193189
}
194190
}
195191

192+
impl fmt::Debug for RollingFileAppender {
193+
// This manual impl is required because of the `now` field (only present
194+
// with `cfg(test)`), which is not `Debug`...
195+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196+
f.debug_struct("RollingFileAppender")
197+
.field("state", &self.state)
198+
.field("writer", &self.writer)
199+
.finish()
200+
}
201+
}
202+
196203
/// Creates a minutely, rolling file appender. This will rotate the log file once per minute.
197204
///
198205
/// The appender returned by `rolling::minutely` can be used with `non_blocking` to create
@@ -469,9 +476,35 @@ impl io::Write for RollingWriter<'_> {
469476
// === impl Inner ===
470477

471478
impl Inner {
472-
fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) {
473-
debug_assert!(self.should_rollover(now));
479+
fn new(
480+
now: OffsetDateTime,
481+
rotation: Rotation,
482+
directory: impl AsRef<Path>,
483+
file_name_prefix: impl AsRef<Path>,
484+
) -> (Self, RwLock<File>) {
485+
let log_directory = directory.as_ref().to_str().unwrap();
486+
let log_filename_prefix = file_name_prefix.as_ref().to_str().unwrap();
474487

488+
let filename = rotation.join_date(log_filename_prefix, &now);
489+
let next_date = rotation.next_date(&now);
490+
let writer = RwLock::new(
491+
create_writer(log_directory, &filename).expect("failed to create appender"),
492+
);
493+
494+
let inner = Inner {
495+
log_directory: log_directory.to_string(),
496+
log_filename_prefix: log_filename_prefix.to_string(),
497+
next_date: AtomicUsize::new(
498+
next_date
499+
.map(|date| date.unix_timestamp() as usize)
500+
.unwrap_or(0),
501+
),
502+
rotation,
503+
};
504+
(inner, writer)
505+
}
506+
507+
fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) {
475508
let filename = self.rotation.join_date(&self.log_filename_prefix, &now);
476509

477510
match create_writer(&self.log_directory, &filename) {
@@ -485,28 +518,36 @@ impl Inner {
485518
}
486519
}
487520

488-
fn should_rollover(&self, date: OffsetDateTime) -> bool {
489-
// the `None` case means that the `InnerAppender` *never* rotates log files.
521+
/// Checks whether or not it's time to roll over the log file.
522+
///
523+
/// Rather than returning a `bool`, this returns the current value of
524+
/// `next_date` so that we can perform a `compare_exchange` operation with
525+
/// that value when setting the next rollover time.
526+
///
527+
/// If this method returns `Some`, we should roll to a new log file.
528+
/// Otherwise, if this returns we should not rotate the log file.
529+
fn should_rollover(&self, date: OffsetDateTime) -> Option<usize> {
490530
let next_date = self.next_date.load(Ordering::Acquire);
531+
// if the next date is 0, this appender *never* rotates log files.
491532
if next_date == 0 {
492-
return false;
533+
return None;
534+
}
535+
536+
if date.unix_timestamp() as usize >= next_date {
537+
return Some(next_date);
493538
}
494-
date.unix_timestamp() as usize >= next_date
539+
540+
None
495541
}
496542

497-
fn advance_date(&self, now: OffsetDateTime) -> bool {
543+
fn advance_date(&self, now: OffsetDateTime, current: usize) -> bool {
498544
let next_date = self
499545
.rotation
500546
.next_date(&now)
501547
.map(|date| date.unix_timestamp() as usize)
502548
.unwrap_or(0);
503549
self.next_date
504-
.compare_exchange(
505-
now.unix_timestamp() as usize,
506-
next_date,
507-
Ordering::AcqRel,
508-
Ordering::Acquire,
509-
)
550+
.compare_exchange(current, next_date, Ordering::AcqRel, Ordering::Acquire)
510551
.is_ok()
511552
}
512553
}
@@ -538,9 +579,10 @@ mod test {
538579

539580
for entry in dir_contents {
540581
let path = entry.expect("Expected dir entry").path();
541-
let result = fs::read_to_string(path).expect("Failed to read file");
582+
let file = fs::read_to_string(&path).expect("Failed to read file");
583+
println!("path={}\nfile={:?}", path.display(), file);
542584

543-
if result.as_str() == expected_value {
585+
if file.as_str() == expected_value {
544586
return true;
545587
}
546588
}
@@ -646,4 +688,79 @@ mod test {
646688
let path = Rotation::NEVER.join_date("app.log", &now);
647689
assert_eq!("app.log", path);
648690
}
691+
692+
#[test]
693+
fn test_make_writer() {
694+
use std::sync::{Arc, Mutex};
695+
use tracing_subscriber::prelude::*;
696+
697+
let format = format_description::parse(
698+
"[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
699+
sign:mandatory]:[offset_minute]:[offset_second]",
700+
)
701+
.unwrap();
702+
703+
let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
704+
let directory = tempfile::tempdir().expect("failed to create tempdir");
705+
let (state, writer) =
706+
Inner::new(now, Rotation::HOURLY, directory.path(), "test_make_writer");
707+
708+
let clock = Arc::new(Mutex::new(now));
709+
let now = {
710+
let clock = clock.clone();
711+
Box::new(move || *clock.lock().unwrap())
712+
};
713+
let appender = RollingFileAppender { state, writer, now };
714+
let default = tracing_subscriber::fmt()
715+
.without_time()
716+
.with_level(false)
717+
.with_target(false)
718+
.with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
719+
.with_writer(appender)
720+
.finish()
721+
.set_default();
722+
723+
tracing::info!("file 1");
724+
725+
// advance time by one second
726+
(*clock.lock().unwrap()) += Duration::seconds(1);
727+
728+
tracing::info!("file 1");
729+
730+
// advance time by one hour
731+
(*clock.lock().unwrap()) += Duration::hours(1);
732+
733+
tracing::info!("file 2");
734+
735+
// advance time by one second
736+
(*clock.lock().unwrap()) += Duration::seconds(1);
737+
738+
tracing::info!("file 2");
739+
740+
drop(default);
741+
742+
let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
743+
println!("dir={:?}", dir_contents);
744+
for entry in dir_contents {
745+
println!("entry={:?}", entry);
746+
let path = entry.expect("Expected dir entry").path();
747+
let file = fs::read_to_string(&path).expect("Failed to read file");
748+
println!("path={}\nfile={:?}", path.display(), file);
749+
750+
match path
751+
.extension()
752+
.expect("found a file without a date!")
753+
.to_str()
754+
.expect("extension should be UTF8")
755+
{
756+
"2020-02-01-10" => {
757+
assert_eq!("file 1\nfile 1\n", file);
758+
}
759+
"2020-02-01-11" => {
760+
assert_eq!("file 2\nfile 2\n", file);
761+
}
762+
x => panic!("unexpected date {}", x),
763+
}
764+
}
765+
}
649766
}

0 commit comments

Comments
 (0)