Skip to content

Commit

Permalink
POC the async update default time zone
Browse files Browse the repository at this point in the history
  • Loading branch information
1996fanrui committed Aug 14, 2024
1 parent d3c0519 commit 4a5a73b
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 13 deletions.
2 changes: 0 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,13 @@ keywords = ["date", "time", "temporal", "zone", "iana"]
edition = "2021"
exclude = ["/.github", "/tmp"]
autotests = false
autoexamples = false
rust-version = "1.70"

[workspace]
members = [
"jiff-cli",
"jiff-tzdb",
"jiff-tzdb-platform",
"examples/*",
]

# Features are documented in the "Crate features" section of the crate docs:
Expand Down
36 changes: 36 additions & 0 deletions examples/default_time_zone_demo/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use jiff::tz::TimeZone;
use std::thread::sleep;
use std::time::Duration;

fn main() {
// The time zone is updated by async thread each 20 seconds.
// And we get the default time zone continuously,
// so we can see the log: `cache is still using, so update the tz.`
for i in 1..=50 {
sleep(Duration::from_secs(1));
TimeZone::system();
println!("round 1------{i}");
}

// Stop the get the default time zone for a while, so we can see the log :
// `cache is not used so far, so stop this thread.`
for i in 1..=35 {
sleep(Duration::from_secs(1));
println!("round 2------{i}");
}

// Get the default time zone again, we can see
// `try_update_time_zone` log to start the thread again.
// And see the `cache is still using, so update the tz.` log later.
for i in 1..=50 {
sleep(Duration::from_secs(1));
TimeZone::system();
println!("round 3------{i}");
}

// See the `cache is not used so far, so stop this thread.` log later.
for i in 1..=50 {
sleep(Duration::from_secs(1));
println!("round 4------{i}");
}
}
93 changes: 82 additions & 11 deletions src/tz/system/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#![allow(dead_code)] // REMOVE ME

use std::{sync::RwLock, time::Duration};

use alloc::{string::ToString, sync::Arc};
use std::{format, println, sync::RwLock, thread, time::Duration};

use crate::{
error::{err, Error, ErrorContext},
tz::{posix::PosixTz, TimeZone, TimeZoneDatabase},
util::cache::Expiration,
};
use alloc::{string::ToString, sync::Arc};
use std::prelude::v1::String;
use std::thread::{sleep, JoinHandle};

#[cfg(unix)]
#[path = "unix.rs"]
Expand Down Expand Up @@ -45,7 +45,9 @@ mod sys {
}

/// The duration of time that a cached time zone should be considered valid.
static TTL: Duration = Duration::new(5 * 60, 0);
const TTL: Duration = Duration::new(20, 0);

const THREAD_NAME: &str = "jiff_time_zone_fetcher";

/// A cached time zone.
///
Expand Down Expand Up @@ -76,13 +78,63 @@ static CACHE: RwLock<Cache> = RwLock::new(Cache::empty());
/// a way to reset this cache and force a re-creation of the time zone.
struct Cache {
tz: Option<TimeZone>,
expiration: Expiration,
ttl: Duration,
// if the cache isn't used after updating, the updating thread will be stopped.
in_use: bool,
handle: Option<JoinHandle<()>>,
}

impl Cache {
/// Create an empty cache. The default state.
const fn empty() -> Cache {
Cache { tz: None, expiration: Expiration::expired() }
Cache { tz: None, ttl: TTL, in_use: false, handle: None }
}

fn try_schedule_update_time_zone(
&mut self,
db: &'static TimeZoneDatabase,
) {
println!("try_update_time_zone");
if self.handle.is_some() {
// Thread race happens here, other thread has already
// started the thread, so return directly.
return;
}
let ttl = self.ttl.clone();
let handle = thread::Builder::new()
.name(THREAD_NAME.to_string())
.spawn(move || {
loop {
sleep(ttl);

let tz_result = get_force(db);
let mut cache = CACHE.write().unwrap();
match tz_result {
Ok(tz) => {
if cache.in_use {
println!(
"cache is still using, so update the tz."
);
cache.tz = Some(tz);
cache.in_use = false;
} else {
println!("cache is not used so far, so stop this thread.");
cache.handle = None;
cache.tz = None;
break;
}
}
Err(_) => {
// Get tz fails, so stop this updating thread.
cache.handle = None;
cache.tz = None;
break;
}
};
}
})
.expect(&format!("failed to spawn {} thread", THREAD_NAME));
self.handle = Some(handle);
}
}

Expand All @@ -99,15 +151,34 @@ impl Cache {
/// it is just impractical to determine the time zone name. For example, when
/// `/etc/localtime` is a hard link to a TZif file instead of a symlink and
/// when the time zone name isn't recorded in any of the other obvious places.
pub(crate) fn get(db: &TimeZoneDatabase) -> Result<TimeZone, Error> {
pub(crate) fn get(db: &'static TimeZoneDatabase) -> Result<TimeZone, Error> {
let mut result: Option<TimeZone> = None;

{
let cache = CACHE.read().unwrap();
if let Some(ref tz) = cache.tz {
if !cache.expiration.is_expired() {
return Ok(tz.clone());
let tz = tz.clone();
if cache.in_use {
// Most of the case: It's already in use, so don't need to update.
return Ok(tz);
} else {
// The first call after updating the timezone.
// update the in_use to true in the next code block(needs write lock).
result = Some(tz);
}
}
}

{
// Cache has tz, but it's not used so far. We need update in_use to true.
if let Some(tz) = result {
let mut cache = CACHE.write().unwrap();
cache.in_use = true;
return Ok(tz);
}
}

// The cache is empty, update the tz and try_schedule_update_time_zone.
let tz = get_force(db)?;
{
// It's okay that we race here. We basically assume that any
Expand All @@ -117,7 +188,7 @@ pub(crate) fn get(db: &TimeZoneDatabase) -> Result<TimeZone, Error> {
// will eventually be true in any sane environment.
let mut cache = CACHE.write().unwrap();
cache.tz = Some(tz.clone());
cache.expiration = Expiration::after(TTL);
cache.try_schedule_update_time_zone(db);
}
Ok(tz)
}
Expand Down

0 comments on commit 4a5a73b

Please sign in to comment.