Skip to content

Commit 4465f02

Browse files
committed
Use chrono to detect local timezone thread-safely
As discussed in PR #44, the 'time' crate fails timezine detection in the prsesense of multiple threads, because the underlying POSIX function localtime_r is not thread safe (at least not on Linux). In order to avoid these thread-safety issues, we use the chrono crate. It avoids system libraries and reimplements timezone detection from scratch. (Thanks to @yaozongyou for pointing this out) TODO: Maybe we should switch to chono entirely. It seems to be a fairly complete replacement. What are the advantages & disadvantages?
1 parent 911a973 commit 4465f02

File tree

3 files changed

+124
-6
lines changed

3 files changed

+124
-6
lines changed

Cargo.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,22 @@ edition = "2018"
2020
rust-version = "1.63"
2121

2222
[features]
23+
# Enabled for compatibility reasons
24+
default = ["localtime"]
2325
nested-values = ["erased-serde", "serde", "serde_json", "slog/nested-values"]
26+
# Automatically detect and use the local timezone for timestamps.
27+
#
28+
# This is enabled by default for compatibility reasons,
29+
# but can be explicitly disabled to reduce dependencies.
30+
#
31+
# This option only affects the default behavior
32+
# and enables support for automatic selection.
33+
#
34+
# Either way an explicit timezone suffix is added for clarity
35+
# UTC: 21:43+00
36+
# MST: 14:43-07
37+
localtime = ["chrono"]
38+
2439

2540
[dependencies]
2641
slog = "2"
@@ -31,6 +46,18 @@ term = "0.7"
3146
erased-serde = {version = "0.3", optional = true }
3247
serde = {version = "1.0", optional = true }
3348
serde_json = {version = "1.0", optional = true }
49+
# We use chrono is to determine the local timezone offset.
50+
# This is because time uses POSIX localtime_r,
51+
# which is not guarenteed to be thread-safe
52+
#
53+
# However, chrono _is_ threadsafe. It works around this issue by
54+
# using a custom reimplementation of the timezone detection logic.
55+
#
56+
# Therefore, whenever the feature 'localtime' is requested,
57+
# we add a dependency on chrono to detect the timezone.
58+
#
59+
# See PR #44 for the original discussion of this issue.
60+
chrono = { version = "0.4", optional = true }d
3461

3562
[dev-dependencies]
3663
slog-async = "2"

src/lib.rs

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ where
356356
}
357357

358358
/// Use the local time zone for the timestamp (default)
359+
#[cfg(feature = "localtime")]
359360
pub fn use_local_timestamp(mut self) -> Self {
360361
self.fn_timestamp = Box::new(timestamp_local);
361362
self
@@ -1089,18 +1090,53 @@ where
10891090
{
10901091
}
10911092

1092-
const TIMESTAMP_FORMAT: &[time::format_description::FormatItem] = time::macros::format_description!("[month repr:short] [day] [hour repr:24]:[minute]:[second].[subsecond digits:3]");
1093+
const TIMESTAMP_FORMAT: &[time::format_description::FormatItem] = time::macros::format_description!(
1094+
concat!(
1095+
"[month repr:short] [day]",
1096+
"[hour repr:24]:[minute]:[second].[subsecond digits:3]",
1097+
// Include timezone by default, to avoid ambiguity
1098+
"[offset_hour sign:mandatory]"
1099+
)
1100+
);
1101+
1102+
/// Convert from [`chrono::DateTime`] into [`time::OffsetDateTime`]
1103+
#[cfg(feature = "localtime")]
1104+
fn local_timestamp_from_chrono(
1105+
local: chrono::DateTime<chrono::Local>
1106+
) -> result::Result<time::OffsetDateTime, TimestampError> {
1107+
use chrono::{Local, Utc};
1108+
let local_time: chrono::DateTime<Local> = Local::now();
1109+
let offset: chrono::FixedOffset = local_time.fixed_offset().timezone();
1110+
let utc_time: chrono::DateTime<Utc> = local_time.to_utc();
1111+
#[cfg(test)] {
1112+
if offset.local_minus_utc() == 0 {
1113+
assert_eq!(utc_time.date_naive(), local_time.date_naive());
1114+
} else {
1115+
assert_ne!(utc_time.date_naive(), local_time.date_naive());
1116+
}
1117+
}
1118+
let utc_time: time::OffsetDateTime = time::OffsetDateTime::from_unix_timestamp(utc_time.timestamp())
1119+
.map_err(LocalTimestampError::UnixTimestamp)
1120+
+ time::Duration::nanoseconds(i64::from(utc_time.timestamp_subsec_nanos()));
1121+
Ok(utc_time.to_offset(time::UtcOffset::from_whole_seconds(offset.local_minus_utc())?))
1122+
}
10931123

10941124
/// Default local timezone timestamp function
10951125
///
10961126
/// The exact format used, is still subject to change.
1127+
///
1128+
/// # Implementation Note
1129+
/// This requires `chrono` to detect the localtime in a thread-safe manner.
1130+
/// See the comment on the feature flag and PR #44
1131+
#[cfg(feature = "localtime")]
10971132
pub fn timestamp_local(io: &mut dyn io::Write) -> io::Result<()> {
1098-
let now: time::OffsetDateTime = std::time::SystemTime::now().into();
1133+
let now = local_timestamp_from_chrono(chrono::Local::now())
1134+
.map_err(TimestampError::LocalConversion)?;
10991135
write!(
11001136
io,
11011137
"{}",
11021138
now.format(TIMESTAMP_FORMAT)
1103-
.map_err(convert_time_fmt_error)?
1139+
.map_err(TimestampError::Format)?
11041140
)
11051141
}
11061142

@@ -1113,11 +1149,44 @@ pub fn timestamp_utc(io: &mut dyn io::Write) -> io::Result<()> {
11131149
io,
11141150
"{}",
11151151
now.format(TIMESTAMP_FORMAT)
1116-
.map_err(convert_time_fmt_error)?
1152+
.map_err(TimestampError::Format)?
11171153
)
11181154
}
1119-
fn convert_time_fmt_error(cause: time::error::Format) -> io::Error {
1120-
io::Error::new(io::ErrorKind::Other, cause)
1155+
1156+
/// An internal error that occurs with timestamps
1157+
#[derive(Debug)]
1158+
#[non_exhaustive]
1159+
enum TimestampError {
1160+
/// An error formatting the timestamp
1161+
Format(time::error::Format),
1162+
/// An error that occurred while
1163+
/// converting from `chrono::DateTime<Local>` to `time::OffsetDateTime`
1164+
LocalTimeConversion(time::error::ComponentRange)
1165+
}
1166+
/*
1167+
* We could use thiserror here,
1168+
* but writing it by hand is almost as good and eliminates a dependency
1169+
*/
1170+
impl fmt::Display for TimestampError {
1171+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1172+
match self {
1173+
TimestampError::Format(cause) => write!(f, "Failed to format timestamp: {cause}"),
1174+
TimestampError::LocalTimeConversion(cause) => write!(f, "Failed to convert local time: {cause}")
1175+
}
1176+
}
1177+
}
1178+
impl From<TimestampError> for io::Error {
1179+
fn from(cause: TimestampError) -> Self {
1180+
io::Error::new(io::ErrorKind::Other, cause)
1181+
}
1182+
}
1183+
impl std::error::Error for TimestampError {
1184+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1185+
match self {
1186+
TimestampError::Format(cause) => Some(cause),
1187+
TimestampError::LocalTimeConversion(cause) => Some(cause),
1188+
}
1189+
}
11211190
}
11221191

11231192
// }}}

src/timestamp.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//! Timestamp logic for slog-term
2+
3+
/// A threadsafe timestamp formatter
4+
///
5+
/// To satify `slog-rs` thread and unwind safety requirements, the
6+
/// bounds expressed by this trait need to satisfied for a function
7+
/// to be used in timestamp formatting.
8+
pub trait TimestampWriter: Sealed + Send + Sync + UnwindSafe + RefUnwindSafe + 'static {
9+
fn write_timestamp(&self, writer: &mut dyn io::Write) -> io::Result<()>;
10+
}
11+
12+
13+
impl<F> ThreadSafeTimestampFn for F
14+
where
15+
F: Fn(&mut dyn io::Write) -> io::Result<()> + Send + Sync,
16+
F: UnwindSafe + RefUnwindSafe + 'static,
17+
F: ?Sized,
18+
{}
19+
20+
mod sealed {
21+
pub trait Sealed {}
22+
}

0 commit comments

Comments
 (0)