Skip to content

Commit

Permalink
Move TimeZoneCache to a separate file (#466)
Browse files Browse the repository at this point in the history
  • Loading branch information
omus authored May 27, 2024
1 parent b6e30cb commit c71bab1
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 79 deletions.
5 changes: 3 additions & 2 deletions src/TimeZones.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export TimeZone, @tz_str, istimezone, FixedTimeZone, VariableTimeZone, ZonedDate

_scratch_dir() = @get_scratch!("build")

const _COMPILED_DIR = Ref{String}()
const _COMPILED_DIR = Ref{String}(TZJData.ARTIFACT_DIR)

# TimeZone types used to disambiguate the context of a DateTime
# abstract type UTC <: TimeZone end # Already defined in the Dates stdlib
Expand All @@ -53,9 +53,10 @@ include("indexable_generator.jl")

include("class.jl")
include("utcoffset.jl")
include(joinpath("types", "timezone.jl"))
include(joinpath("types", "fixedtimezone.jl"))
include(joinpath("types", "variabletimezone.jl"))
include(joinpath("types", "timezone.jl"))
include(joinpath("types", "timezonecache.jl"))
include(joinpath("types", "zoneddatetime.jl"))
include(joinpath("tzfile", "TZFile.jl"))
include(joinpath("tzjfile", "TZJFile.jl"))
Expand Down
77 changes: 0 additions & 77 deletions src/types/timezone.jl
Original file line number Diff line number Diff line change
@@ -1,80 +1,3 @@
# Use a separate cache for FixedTimeZone (which is `isbits`) so the container is concretely
# typed and we avoid allocating a FixedTimeZone every time we get one from the cache.
struct TimeZoneCache
ftz::Dict{String,Tuple{FixedTimeZone,Class}}
vtz::Dict{String,Tuple{VariableTimeZone,Class}}
lock::ReentrantLock
initialized::Threads.Atomic{Bool}
end

TimeZoneCache() = TimeZoneCache(Dict(), Dict(), ReentrantLock(), Threads.Atomic{Bool}(false))

# Retains the compiled tzdata in memory. Read-only access to the cache is thread-safe and
# any changes to this structure can result in inconsistent behaviour. Do not access this
# object directly, instead use `get` to access the cache content.
const _TZ_CACHE = TimeZoneCache()

function Base.copy!(dst::TimeZoneCache, src::TimeZoneCache)
copy!(dst.ftz, src.ftz)
copy!(dst.vtz, src.vtz)
dst.initialized[] = src.initialized[]
return dst
end

function reload!(cache::TimeZoneCache, compiled_dir::AbstractString=_COMPILED_DIR[])
empty!(cache.ftz)
empty!(cache.vtz)

walk_tz_dir(compiled_dir) do name, path
tz, class = open(TZJFile.read, path, "r")(name)

if tz isa FixedTimeZone
cache.ftz[name] = (tz, class)
elseif tz isa VariableTimeZone
cache.vtz[name] = (tz, class)
else
error("Unhandled TimeZone class encountered: $(typeof(tz))")
end
end

!isempty(cache.ftz) && !isempty(cache.vtz) || error("Cache remains empty after loading")

return cache
end

function Base.get(body::Function, cache::TimeZoneCache, name::AbstractString)
if !cache.initialized[]
lock(cache.lock) do
if !cache.initialized[]
_initialize()
reload!(cache)
cache.initialized[] = true
end
end
end

return get(cache.ftz, name) do
get(cache.vtz, name) do
body()
end
end
end

function _initialize()
# Write out our compiled tzdata representations into a scratchspace
desired_version = TZData.tzdata_version()

_COMPILED_DIR[] = if desired_version == TZJData.TZDATA_VERSION
TZJData.ARTIFACT_DIR
else
TZData.build(desired_version, _scratch_dir())
end

return nothing
end

_reload_tz_cache(compiled_dir::AbstractString) = reload!(_TZ_CACHE, compiled_dir)

"""
TimeZone(str::AbstractString) -> TimeZone
Expand Down
73 changes: 73 additions & 0 deletions src/types/timezonecache.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Use a separate cache for FixedTimeZone (which is `isbits`) so the container is concretely
# typed and we avoid allocating a FixedTimeZone every time we get one from the cache.
struct TimeZoneCache
ftz::Dict{String,Tuple{FixedTimeZone,Class}}
vtz::Dict{String,Tuple{VariableTimeZone,Class}}
lock::ReentrantLock
initialized::Threads.Atomic{Bool}
end

TimeZoneCache() = TimeZoneCache(Dict(), Dict(), ReentrantLock(), Threads.Atomic{Bool}(false))

# Retains the compiled tzdata in memory. Read-only access to the cache is thread-safe and
# any changes to this structure can result in inconsistent behaviour. Do not access this
# object directly, instead use `get` to access the cache content.
const _TZ_CACHE = TimeZoneCache()

function Base.copy!(dst::TimeZoneCache, src::TimeZoneCache)
copy!(dst.ftz, src.ftz)
copy!(dst.vtz, src.vtz)
dst.initialized[] = src.initialized[]
return dst
end

function reload!(cache::TimeZoneCache, compiled_dir::AbstractString=_COMPILED_DIR[])
empty!(cache.ftz)
empty!(cache.vtz)

walk_tz_dir(compiled_dir) do name, path
tz, class = open(TZJFile.read, path, "r")(name)

if tz isa FixedTimeZone
cache.ftz[name] = (tz, class)
elseif tz isa VariableTimeZone
cache.vtz[name] = (tz, class)
else
error("Unhandled TimeZone class encountered: $(typeof(tz))")
end
end

!isempty(cache.ftz) && !isempty(cache.vtz) || error("Cache remains empty after loading")

return cache
end

function Base.get(body::Function, cache::TimeZoneCache, name::AbstractString)
if !cache.initialized[]
lock(cache.lock) do
if !cache.initialized[]
_build()
reload!(cache)
cache.initialized[] = true
end
end
end

return get(cache.ftz, name) do
get(cache.vtz, name) do
body()
end
end
end

# Build specific tzdata version if specified by `JULIA_TZ_VERSION`
function _build()
desired_version = TZData.tzdata_version()
if desired_version != TZJData.TZDATA_VERSION
_COMPILED_DIR[] = TZData.build(desired_version, _scratch_dir())
end

return nothing
end

_reload_tz_cache(compiled_dir::AbstractString) = reload!(_TZ_CACHE, compiled_dir)

0 comments on commit c71bab1

Please sign in to comment.