Skip to content

Commit f362f47

Browse files
authored
precompile: fix a myriad of concurrency and cache handling mistakes (#59670)
* The PR for LOADING_CACHE failed to acquire the lock in many places it was newly made mandatory. * The stale_cache_key did not include many relevant parameters. * The `isprecompiled` implementation failed to account for the preferred stdlib logic of loading. * The `isprecompiled` cache failed to account for either newly compiled packages or other changes to the file system. * After parallel precompile noticed a package fails, it would retry with serial precompile, which was very pointless. * Semaphore could be <= 0, leading to deadlock. * After parallel precompile noticed a failure, it would crash, instead of allowing the program to continue on to load the package normally. We had tests for this, but they had gotten removed.
1 parent cf321d0 commit f362f47

File tree

4 files changed

+192
-116
lines changed

4 files changed

+192
-116
lines changed

base/loading.jl

Lines changed: 85 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ struct LoadingCache
260260
identified::Dict{String, Union{Nothing, Tuple{PkgId, String}}}
261261
located::Dict{Tuple{PkgId, Union{String, Nothing}}, Union{Tuple{String, String}, Nothing}}
262262
end
263-
const LOADING_CACHE = Ref{Union{LoadingCache, Nothing}}(nothing)
263+
const LOADING_CACHE = Ref{Union{LoadingCache, Nothing}}(nothing) # n.b.: all access to and through this are protected by require_lock
264264
LoadingCache() = LoadingCache(load_path(), Dict(), Dict(), Dict(), Set(), Dict(), Dict(), Dict())
265265

266266

@@ -302,10 +302,12 @@ end
302302

303303
# Used by Pkg but not used in loading itself
304304
function find_package(arg) # ::Union{Nothing,String}
305+
@lock require_lock begin
305306
pkgenv = identify_package_env(arg)
306307
pkgenv === nothing && return nothing
307308
pkg, env = pkgenv
308309
return locate_package(pkg, env)
310+
end
309311
end
310312

311313
# is there a better/faster ground truth?
@@ -332,6 +334,7 @@ is also returned, except when the identity is not identified.
332334
"""
333335
identify_package_env(where::Module, name::String) = identify_package_env(PkgId(where), name)
334336
function identify_package_env(where::PkgId, name::String)
337+
assert_havelock(require_lock)
335338
cache = LOADING_CACHE[]
336339
if cache !== nothing
337340
pkg_env = get(cache.identified_where, (where, name), missing)
@@ -364,6 +367,7 @@ function identify_package_env(where::PkgId, name::String)
364367
return pkg_env
365368
end
366369
function identify_package_env(name::String)
370+
assert_havelock(require_lock)
367371
cache = LOADING_CACHE[]
368372
if cache !== nothing
369373
pkg_env = get(cache.identified, name, missing)
@@ -426,11 +430,12 @@ julia> using LinearAlgebra
426430
julia> Base.identify_package(LinearAlgebra, "Pkg") # Pkg is not a dependency of LinearAlgebra
427431
```
428432
"""
429-
identify_package(where::Module, name::String) = _nothing_or_first(identify_package_env(where, name))
430-
identify_package(where::PkgId, name::String) = _nothing_or_first(identify_package_env(where, name))
431-
identify_package(name::String) = _nothing_or_first(identify_package_env(name))
433+
identify_package(where::Module, name::String) = @lock require_lock _nothing_or_first(identify_package_env(where, name))
434+
identify_package(where::PkgId, name::String) = @lock require_lock _nothing_or_first(identify_package_env(where, name))
435+
identify_package(name::String) = @lock require_lock _nothing_or_first(identify_package_env(name))
432436

433437
function locate_package_env(pkg::PkgId, stopenv::Union{String, Nothing}=nothing)::Union{Nothing,Tuple{String,String}}
438+
assert_havelock(require_lock)
434439
cache = LOADING_CACHE[]
435440
if cache !== nothing
436441
pathenv = get(cache.located, (pkg, stopenv), missing)
@@ -508,7 +513,7 @@ julia> Base.locate_package(pkg)
508513
```
509514
"""
510515
function locate_package(pkg::PkgId, stopenv::Union{String, Nothing}=nothing)::Union{Nothing,String}
511-
_nothing_or_first(locate_package_env(pkg, stopenv))
516+
@lock require_lock _nothing_or_first(locate_package_env(pkg, stopenv))
512517
end
513518

514519
"""
@@ -1824,51 +1829,60 @@ function show(io::IO, it::ImageTarget)
18241829
end
18251830

18261831
# should sync with the types of arguments of `stale_cachefile`
1827-
const StaleCacheKey = Tuple{PkgId, UInt128, String, String}
1832+
const StaleCacheKey = Tuple{PkgId, UInt128, String, String, Bool, CacheFlags}
18281833

1829-
function compilecache_path(pkg::PkgId;
1834+
function compilecache_freshest_path(pkg::PkgId;
18301835
ignore_loaded::Bool=false,
18311836
stale_cache::Dict{StaleCacheKey,Bool}=Dict{StaleCacheKey, Bool}(),
18321837
cachepath_cache::Dict{PkgId, Vector{String}}=Dict{PkgId, Vector{String}}(),
1833-
cachepaths::Vector{String}=get!(() -> find_all_in_cache_path(pkg), cachepath_cache, pkg),
1838+
cachepaths::Vector{String}=get(() -> find_all_in_cache_path(pkg), cachepath_cache, pkg),
18341839
sourcepath::Union{String,Nothing}=Base.locate_package(pkg),
18351840
flags::CacheFlags=CacheFlags())
1836-
path = nothing
18371841
isnothing(sourcepath) && error("Cannot locate source for $(repr("text/plain", pkg))")
1838-
for path_to_try in cachepaths
1839-
staledeps = stale_cachefile(sourcepath, path_to_try; ignore_loaded, requested_flags=flags)
1840-
if staledeps === true
1841-
continue
1842-
end
1843-
staledeps, _, _ = staledeps::Tuple{Vector{Any}, Union{Nothing, String}, UInt128}
1844-
# finish checking staledeps module graph
1845-
for dep in staledeps
1846-
dep isa Module && continue
1847-
modpath, modkey, modbuild_id = dep::Tuple{String, PkgId, UInt128}
1848-
modpaths = get!(() -> find_all_in_cache_path(modkey), cachepath_cache, modkey)
1849-
for modpath_to_try in modpaths::Vector{String}
1850-
stale_cache_key = (modkey, modbuild_id, modpath, modpath_to_try)::StaleCacheKey
1851-
if get!(() -> stale_cachefile(stale_cache_key...; ignore_loaded, requested_flags=flags) === true,
1852-
stale_cache, stale_cache_key)
1853-
continue
1842+
try_build_ids = UInt128[UInt128(0)]
1843+
if !ignore_loaded
1844+
let loaded = get(loaded_precompiles, pkg, nothing)
1845+
if loaded !== nothing
1846+
for mod in loaded # try these in reverse original load order to see if one is already valid
1847+
pushfirst!(try_build_ids, module_build_id(mod))
18541848
end
1855-
@goto check_next_dep
18561849
end
1857-
@goto check_next_path
1858-
@label check_next_dep
18591850
end
1860-
try
1861-
# update timestamp of precompilation file so that it is the first to be tried by code loading
1862-
touch(path_to_try)
1863-
catch ex
1864-
# file might be read-only and then we fail to update timestamp, which is fine
1865-
ex isa IOError || rethrow()
1851+
end
1852+
for build_id in try_build_ids
1853+
for path_to_try in cachepaths
1854+
staledeps = stale_cachefile(pkg, build_id, sourcepath, path_to_try; ignore_loaded, requested_flags=flags)
1855+
if staledeps === true
1856+
continue
1857+
end
1858+
staledeps, _, _ = staledeps::Tuple{Vector{Any}, Union{Nothing, String}, UInt128}
1859+
# finish checking staledeps module graph
1860+
for dep in staledeps
1861+
dep isa Module && continue
1862+
modpath, modkey, modbuild_id = dep::Tuple{String, PkgId, UInt128}
1863+
modpaths = get(() -> find_all_in_cache_path(modkey), cachepath_cache, modkey)
1864+
for modpath_to_try in modpaths::Vector{String}
1865+
stale_cache_key = (modkey, modbuild_id, modpath, modpath_to_try, ignore_loaded, flags)::StaleCacheKey
1866+
if get!(() -> stale_cachefile(modkey, modbuild_id, modpath, modpath_to_try; ignore_loaded, requested_flags=flags) === true,
1867+
stale_cache, stale_cache_key)
1868+
continue
1869+
end
1870+
@goto check_next_dep
1871+
end
1872+
@goto check_next_path
1873+
@label check_next_dep
1874+
end
1875+
try
1876+
# update timestamp of precompilation file so that it is the first to be tried by code loading
1877+
touch(path_to_try)
1878+
catch ex
1879+
# file might be read-only and then we fail to update timestamp, which is fine
1880+
ex isa IOError || rethrow()
1881+
end
1882+
return path_to_try
1883+
@label check_next_path
18661884
end
1867-
path = path_to_try
1868-
break
1869-
@label check_next_path
18701885
end
1871-
return path
18721886
end
18731887

18741888
"""
@@ -1884,14 +1898,8 @@ fresh julia session specify `ignore_loaded=true`.
18841898
!!! compat "Julia 1.10"
18851899
This function requires at least Julia 1.10.
18861900
"""
1887-
function isprecompiled(pkg::PkgId;
1888-
ignore_loaded::Bool=false,
1889-
stale_cache::Dict{StaleCacheKey,Bool}=Dict{StaleCacheKey, Bool}(),
1890-
cachepath_cache::Dict{PkgId, Vector{String}}=Dict{PkgId, Vector{String}}(),
1891-
cachepaths::Vector{String}=get!(() -> find_all_in_cache_path(pkg), cachepath_cache, pkg),
1892-
sourcepath::Union{String,Nothing}=Base.locate_package(pkg),
1893-
flags::CacheFlags=CacheFlags())
1894-
path = compilecache_path(pkg; ignore_loaded, stale_cache, cachepath_cache, cachepaths, sourcepath, flags)
1901+
function isprecompiled(pkg::PkgId; ignore_loaded::Bool=false)
1902+
path = compilecache_freshest_path(pkg; ignore_loaded)
18951903
return !isnothing(path)
18961904
end
18971905

@@ -1905,7 +1913,7 @@ associated cache is relocatable.
19051913
This function requires at least Julia 1.11.
19061914
"""
19071915
function isrelocatable(pkg::PkgId)
1908-
path = compilecache_path(pkg)
1916+
path = compilecache_freshest_path(pkg)
19091917
isnothing(path) && return false
19101918
io = open(path, "r")
19111919
try
@@ -1925,6 +1933,23 @@ function isrelocatable(pkg::PkgId)
19251933
return true
19261934
end
19271935

1936+
function parse_cache_buildid(cachepath::String)
1937+
f = open(cachepath, "r")
1938+
try
1939+
checksum = isvalid_cache_header(f)
1940+
iszero(checksum) && throw(ArgumentError("Incompatible header in cache file $cachefile."))
1941+
flags = read(f, UInt8)
1942+
n = read(f, Int32)
1943+
n == 0 && error("no module defined in $cachefile")
1944+
skip(f, n) # module name
1945+
uuid = UUID((read(f, UInt64), read(f, UInt64))) # pkg UUID
1946+
build_id = (UInt128(checksum) << 64) | read(f, UInt64)
1947+
return build_id, uuid
1948+
finally
1949+
close(f)
1950+
end
1951+
end
1952+
19281953
# search for a precompile cache file to load, after some various checks
19291954
function _tryrequire_from_serialized(modkey::PkgId, build_id::UInt128)
19301955
assert_havelock(require_lock)
@@ -2682,8 +2707,19 @@ function __require_prelocked(pkg::PkgId, env)
26822707
try
26832708
if !generating_output() && !parallel_precompile_attempted && !disable_parallel_precompile && @isdefined(Precompilation)
26842709
parallel_precompile_attempted = true
2685-
Precompilation.precompilepkgs([pkg]; _from_loading=true, ignore_loaded=false)
2686-
return
2710+
precompiled = Precompilation.precompilepkgs([pkg]; _from_loading=true, ignore_loaded=false)
2711+
# prcompiled returns either nothing, indicating it needs serial precompile,
2712+
# or the entry(ies) that it found would be best to load (possibly because it just created it)
2713+
# or an empty set of entries (indicating the precompile should be skipped)
2714+
if precompiled !== nothing
2715+
isempty(precompiled) && return PrecompilableError() # oops, Precompilation forgot to report what this might actually be
2716+
local cachefile = precompiled[1]
2717+
local ocachefile = nothing
2718+
if JLOptions().use_pkgimages == 1
2719+
ocachefile = ocachefile_from_cachefile(cachefile)
2720+
end
2721+
return cachefile, ocachefile
2722+
end
26872723
end
26882724
triggers = get(EXT_PRIMED, pkg, nothing)
26892725
loadable_exts = nothing

0 commit comments

Comments
 (0)