Skip to content

Commit

Permalink
use thread_local cache of parsed version of previous TzInfo file or T…
Browse files Browse the repository at this point in the history
…Z variable

minor fixes

improve ergonomics, cache mtime for 1s

1.32 fixes

avoid excess Cache::default() calls

add back cfg_attr
  • Loading branch information
esheppa committed Jul 25, 2022
1 parent acd4ecf commit 44e02a9
Showing 1 changed file with 106 additions and 36 deletions.
142 changes: 106 additions & 36 deletions src/offset/local/unix.rs
Expand Up @@ -8,64 +8,134 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use std::sync::Once;
use std::{cell::RefCell, env, fs, time::SystemTime};

use super::tz_info::TimeZone;
use super::{DateTime, FixedOffset, Local, NaiveDateTime};
use crate::{Datelike, LocalResult, Utc};

pub(super) fn now() -> DateTime<Local> {
let now = Utc::now().naive_utc();
DateTime::from_utc(now, offset(now, false).unwrap())
naive_to_local(&now, false).unwrap()
}

pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> LocalResult<DateTime<Local>> {
if local {
match offset(*d, true) {
LocalResult::None => LocalResult::None,
LocalResult::Ambiguous(early, late) => LocalResult::Ambiguous(
DateTime::from_utc(*d - early, early),
DateTime::from_utc(*d - late, late),
),
LocalResult::Single(offset) => {
LocalResult::Single(DateTime::from_utc(*d - offset, offset))
TZ_INFO.with(|maybe_cache| {
maybe_cache.borrow_mut().get_or_insert_with(Cache::default).offset(*d, local)
})
}

// we have to store the `Cache` in an option as it can't
// be initalized in a static context.
thread_local! {
static TZ_INFO: RefCell<Option<Cache>> = Default::default();
}

enum Source {
LocalTime { mtime: SystemTime, last_checked: SystemTime },
// we don't bother storing the contents of the environment variable in this case.
// changing the environment while the process is running is generally not reccomended
Environment,
}

impl Default for Source {
fn default() -> Source {
// use of var_os avoids allocating, which is nice
// as we are only going to discard the string anyway
// but we must ensure the contents are valid unicode
// otherwise the behaivour here would be different
// to that in `naive_to_local`
match env::var_os("TZ") {
Some(ref s) if s.to_str().is_some() => Source::Environment,
Some(_) | None => Source::LocalTime {
mtime: fs::symlink_metadata("/etc/localtime")
.expect("localtime should exist")
.modified()
.unwrap(),
last_checked: SystemTime::now(),
},
}
}
}

impl Source {
fn out_of_date(&mut self) -> bool {
let now = SystemTime::now();
let prev = match self {
Source::LocalTime { mtime, last_checked } => match now.duration_since(*last_checked) {
Ok(d) if d.as_secs() < 1 => return false,
Ok(_) | Err(_) => *mtime,
},
Source::Environment => return false,
};

match Source::default() {
Source::LocalTime { mtime, .. } => {
*self = Source::LocalTime { mtime, last_checked: now };
prev != mtime
}
// will only reach here if TZ has been set while
// the process is running
Source::Environment => {
*self = Source::Environment;
true
}
}
} else {
LocalResult::Single(DateTime::from_utc(*d, offset(*d, false).unwrap()))
}
}

fn offset(d: NaiveDateTime, local: bool) -> LocalResult<FixedOffset> {
let info = unsafe {
INIT.call_once(|| {
INFO = Some(TimeZone::local().expect("unable to parse localtime info"));
});
INFO.as_ref().unwrap()
};
struct Cache {
zone: TimeZone,
source: Source,
}

impl Default for Cache {
fn default() -> Cache {
Cache {
zone: TimeZone::local().expect("unable to parse localtime info"),
source: Source::default(),
}
}
}

impl Cache {
fn offset(&mut self, d: NaiveDateTime, local: bool) -> LocalResult<DateTime<Local>> {
if self.source.out_of_date() {
*self = Cache::default();
}

if !local {
let offset = FixedOffset::east(
self.zone
.find_local_time_type(d.timestamp())
.expect("unable to select local time type")
.offset(),
);

return LocalResult::Single(DateTime::from_utc(d, offset));
}

if local {
// we pass through the year as the year of a local point in time must either be valid in that locale, or
// the entire time was skipped in which case we will return LocalResult::None anywa.
match info
match self
.zone
.find_local_time_type_from_local(d.timestamp(), d.year())
.expect("unable to select local time type")
{
LocalResult::None => LocalResult::None,
LocalResult::Ambiguous(early, late) => LocalResult::Ambiguous(
FixedOffset::east(early.offset()),
FixedOffset::east(late.offset()),
),
LocalResult::Single(tt) => LocalResult::Single(FixedOffset::east(tt.offset())),
LocalResult::Ambiguous(early, late) => {
let early_offset = FixedOffset::east(early.offset());
let late_offset = FixedOffset::east(late.offset());

LocalResult::Ambiguous(
DateTime::from_utc(d - early_offset, early_offset),
DateTime::from_utc(d - late_offset, late_offset),
)
}
LocalResult::Single(tt) => {
let offset = FixedOffset::east(tt.offset());
LocalResult::Single(DateTime::from_utc(d - offset, offset))
}
}
} else {
LocalResult::Single(FixedOffset::east(
info.find_local_time_type(d.timestamp())
.expect("unable to select local time type")
.offset(),
))
}
}

static mut INFO: Option<TimeZone> = None;
static INIT: Once = Once::new();

0 comments on commit 44e02a9

Please sign in to comment.