Skip to content

Commit 70cdb68

Browse files
committed
Use chrono to detect local timezone thread-safely
As discussed in PR #44, the 'time' crate fails timezone detection in the presence of multiple threads, because the underlying POSIX function localtime_r is not thread safe. 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 suggesting this fix. There are two improvements that could be made to reduce dependency bloat: - Only enable this on unix systems - Switch to chono entirely. However, this fix is sufficient to avoid the bug and that is the priority. It is possible that the optimizer is smart enough to remove all chrono code except the localtime detection functionality. In that case, switching from time to chrono could increase bloat by pulling in two copies of time formatting code. Tests/benchmarks are needed to determine the best approach.
1 parent a86edea commit 70cdb68

File tree

3 files changed

+91
-5
lines changed

3 files changed

+91
-5
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77
<!-- next-url -->
88
## [Unreleased](https://github.com/slog-rs/term/compare/v2.8.1...HEAD) - ReleaseDate
99

10+
### Fixed
11+
* Avoid thread-safety issues with `localtime_r` function by using `chrono` to detect local time.
12+
* Unfortunately this adds another dependency
13+
In the future we could reduce this by switching from time to chrono.
14+
1015
## 2.9.1 - 2024-02-18
1116
### Fixed
1217
* Switch from `atty` to `is_terminal`

Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ term = "0.7"
3131
erased-serde = {version = "0.3", optional = true }
3232
serde = {version = "1.0", optional = true }
3333
serde_json = {version = "1.0", optional = true }
34+
# We use chrono is to determine the local timezone offset.
35+
# This is because time uses POSIX localtime_r,
36+
# which is not guaranteed to be thread-safe
37+
#
38+
# However, chrono _is_ threadsafe. It works around this issue by
39+
# using a custom reimplementation of the timezone detection logic.
40+
#
41+
# Therefore, whenever the feature 'localtime' is requested,
42+
# we add a dependency on chrono to detect the timezone.
43+
#
44+
# See PR #44 for the original discussion of this issue.
45+
chrono = "0.4"
3446

3547
[dev-dependencies]
3648
slog-async = "2"

src/lib.rs

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,16 +1091,48 @@ where
10911091

10921092
const TIMESTAMP_FORMAT: &[time::format_description::FormatItem] = time::macros::format_description!("[month repr:short] [day] [hour repr:24]:[minute]:[second].[subsecond digits:3]");
10931093

1094+
/// Convert from [`chrono::DateTime`] into [`time::OffsetDateTime`]
1095+
fn local_timestamp_from_chrono(
1096+
local: chrono::DateTime<chrono::Local>,
1097+
) -> result::Result<time::OffsetDateTime, TimestampError> {
1098+
use chrono::{Local, Utc};
1099+
let local_time: chrono::DateTime<Local> = Local::now();
1100+
let offset: chrono::FixedOffset = local_time.fixed_offset().timezone();
1101+
let utc_time: chrono::DateTime<Utc> = local_time.to_utc();
1102+
#[cfg(test)]
1103+
{
1104+
if offset.local_minus_utc() == 0 {
1105+
assert_eq!(utc_time.to_rfc3339(), local_time.to_rfc3339());
1106+
} else {
1107+
assert_ne!(utc_time.to_rfc3339(), local_time.to_rfc3339());
1108+
}
1109+
}
1110+
let utc_time: time::OffsetDateTime =
1111+
time::OffsetDateTime::from_unix_timestamp(utc_time.timestamp())
1112+
.map_err(TimestampError::LocalTimeConversion)?
1113+
+ time::Duration::nanoseconds(i64::from(
1114+
utc_time.timestamp_subsec_nanos(),
1115+
));
1116+
Ok(utc_time.to_offset(
1117+
time::UtcOffset::from_whole_seconds(offset.local_minus_utc())
1118+
.map_err(TimestampError::LocalTimeConversion)?,
1119+
))
1120+
}
1121+
10941122
/// Default local timezone timestamp function
10951123
///
10961124
/// The exact format used, is still subject to change.
1125+
///
1126+
/// # Implementation Note
1127+
/// This requires `chrono` to detect the localtime in a thread-safe manner.
1128+
/// See the comment on the dependency, PR #44 and PR #48.
10971129
pub fn timestamp_local(io: &mut dyn io::Write) -> io::Result<()> {
1098-
let now: time::OffsetDateTime = std::time::SystemTime::now().into();
1130+
let now = local_timestamp_from_chrono(chrono::Local::now())?;
10991131
write!(
11001132
io,
11011133
"{}",
11021134
now.format(TIMESTAMP_FORMAT)
1103-
.map_err(convert_time_fmt_error)?
1135+
.map_err(TimestampError::Format)?
11041136
)
11051137
}
11061138

@@ -1113,11 +1145,48 @@ pub fn timestamp_utc(io: &mut dyn io::Write) -> io::Result<()> {
11131145
io,
11141146
"{}",
11151147
now.format(TIMESTAMP_FORMAT)
1116-
.map_err(convert_time_fmt_error)?
1148+
.map_err(TimestampError::Format)?
11171149
)
11181150
}
1119-
fn convert_time_fmt_error(cause: time::error::Format) -> io::Error {
1120-
io::Error::new(io::ErrorKind::Other, cause)
1151+
1152+
/// An internal error that occurs with timestamps
1153+
#[derive(Debug)]
1154+
#[non_exhaustive]
1155+
enum TimestampError {
1156+
/// An error formatting the timestamp
1157+
Format(time::error::Format),
1158+
/// An error that occurred while
1159+
/// converting from `chrono::DateTime<Local>` to `time::OffsetDateTime`
1160+
LocalTimeConversion(time::error::ComponentRange),
1161+
}
1162+
/*
1163+
* We could use thiserror here,
1164+
* but writing it by hand is almost as good and eliminates a dependency
1165+
*/
1166+
impl fmt::Display for TimestampError {
1167+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1168+
match self {
1169+
TimestampError::Format(cause) => {
1170+
write!(f, "Failed to format timestamp: {cause}")
1171+
}
1172+
TimestampError::LocalTimeConversion(cause) => {
1173+
write!(f, "Failed to convert local time: {cause}")
1174+
}
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
// }}}

0 commit comments

Comments
 (0)