Skip to content

Commit 251dbef

Browse files
committed
ensure extension triggers are only run by the package that satified them
Oscar had noticed a problem where loading an extension might trigger loading another package, which might re-trigger attempting to load the same extension. And then that causes a deadlock from waiting for the extension to finish loading (it appears to be a recursive import triggered from multiple places). Instead alter the representation, to be similar to a semaphore, so that it will be loaded only exactly by the final package that satisfied all dependencies for it. This approach could still encounter an issue if the user imports a package (C) which it does not explicitly list as a dependency for extension. But it is unclear to me that we actually want to solve that, since it weakens and delays the premise that Bext is available shortly after A and B are both loaded. # module C; using A, B; end;; module A; end;; module B; end;; module Bext; using C; end # using C, Bext / A / B starts C -> requires A, B to load loads A -> defines Bext (extends B, but tries to also require C) loads B -> loads Bext (whhich waits for C -> deadlock!) finish C -> now safe to load Bext # while using this order would have been fine # using A, B, Bext / C loads A -> defines Bext (extends B, but tries to also require C) loads B -> starts Bext loads C finish Bext
1 parent bebff47 commit 251dbef

File tree

1 file changed

+85
-49
lines changed

1 file changed

+85
-49
lines changed

base/loading.jl

Lines changed: 85 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,9 +1076,9 @@ function register_restored_modules(sv::SimpleVector, pkg::PkgId, path::String)
10761076
end
10771077

10781078
function run_package_callbacks(modkey::PkgId)
1079+
run_extension_callbacks(modkey)
10791080
assert_havelock(require_lock)
10801081
unlock(require_lock)
1081-
run_extension_callbacks()
10821082
try
10831083
for callback in package_callbacks
10841084
invokelatest(callback, modkey)
@@ -1099,14 +1099,13 @@ end
10991099
##############
11001100

11011101
mutable struct ExtensionId
1102-
const id::PkgId # Could be symbol?
1103-
const parentid::PkgId
1104-
const triggers::Vector{PkgId} # What packages have to be loaded for the extension to get loaded
1105-
triggered::Bool
1106-
succeeded::Bool
1102+
const id::PkgId
1103+
const parentid::PkgId # just need the name, for printing
1104+
ntriggers::Int # how many more packages must be defined until this is loaded
11071105
end
11081106

1109-
const EXT_DORMITORY = ExtensionId[]
1107+
const EXT_DORMITORY = Dict{PkgId,Vector{ExtensionId}}()
1108+
const EXT_DORMITORY_FAILED = ExtensionId[]
11101109

11111110
function insert_extension_triggers(pkg::PkgId)
11121111
pkg.uuid === nothing && return
@@ -1159,59 +1158,84 @@ end
11591158
function _insert_extension_triggers(parent::PkgId, extensions::Dict{String, <:Any}, weakdeps::Dict{String, <:Any})
11601159
for (ext::String, triggers::Union{String, Vector{String}}) in extensions
11611160
triggers isa String && (triggers = [triggers])
1162-
triggers_id = PkgId[]
11631161
id = PkgId(uuid5(parent.uuid, ext), ext)
1162+
gid = ExtensionId(id, parent, 1 + length(triggers))
1163+
trigger1 = get!(Vector{ExtensionId}, EXT_DORMITORY, parent)
1164+
push!(trigger1, gid)
11641165
for trigger in triggers
11651166
# TODO: Better error message if this lookup fails?
11661167
uuid_trigger = UUID(weakdeps[trigger]::String)
1167-
push!(triggers_id, PkgId(uuid_trigger, trigger))
1168+
trigger_id = PkgId(uuid_trigger, trigger)
1169+
if !haskey(Base.loaded_modules, trigger_id) || haskey(package_locks, trigger_id)
1170+
trigger1 = get!(Vector{ExtensionId}, EXT_DORMITORY, trigger_id)
1171+
push!(trigger1, gid)
1172+
else
1173+
gid.ntriggers -= 1
1174+
end
11681175
end
1169-
gid = ExtensionId(id, parent, triggers_id, false, false)
1170-
push!(EXT_DORMITORY, gid)
11711176
end
11721177
end
11731178

1174-
function run_extension_callbacks(; force::Bool=false)
1175-
try
1176-
# TODO, if `EXT_DORMITORY` becomes very long, do something smarter
1177-
for extid in EXT_DORMITORY
1178-
extid.succeeded && continue
1179-
!force && extid.triggered && continue
1180-
if all(x -> haskey(Base.loaded_modules, x) && !haskey(package_locks, x), extid.triggers)
1181-
ext_not_allowed_load = nothing
1182-
extid.triggered = true
1183-
# It is possible that some of the triggers were loaded in an environment
1184-
# below the one of the parent. This will cause a load failure when the
1185-
# pkg ext tries to load the triggers. Therefore, check this first
1186-
# before loading the pkg ext.
1187-
for trigger in extid.triggers
1188-
pkgenv = Base.identify_package_env(extid.id, trigger.name)
1189-
if pkgenv === nothing
1190-
ext_not_allowed_load = trigger
1191-
break
1192-
else
1193-
pkg, env = pkgenv
1194-
path = Base.locate_package(pkg, env)
1195-
if path === nothing
1196-
ext_not_allowed_load = trigger
1197-
break
1198-
end
1199-
end
1200-
end
1201-
if ext_not_allowed_load !== nothing
1202-
@debug "Extension $(extid.id.name) of $(extid.parentid.name) not loaded due to \
1203-
$(ext_not_allowed_load.name) loaded in environment lower in load path"
1204-
else
1205-
require(extid.id)
1206-
@debug "Extension $(extid.id.name) of $(extid.parentid.name) loaded"
1207-
end
1208-
extid.succeeded = true
1209-
end
1210-
end
1179+
function run_extension_callbacks(extid::ExtensionId)
1180+
assert_havelock(require_lock)
1181+
succeeded = try
1182+
_require_prelocked(extid.id)
1183+
@debug "Extension $(extid.id.name) of $(extid.parentid.name) loaded"
1184+
true
12111185
catch
12121186
# Try to continue loading if loading an extension errors
12131187
errs = current_exceptions()
12141188
@error "Error during loading of extension" exception=errs
1189+
false
1190+
end
1191+
return succeeded
1192+
end
1193+
1194+
function run_extension_callbacks(pkgid::PkgId)
1195+
assert_havelock(require_lock)
1196+
# take ownership of extids that depend on this pkgid
1197+
extids = pop!(EXT_DORMITORY, pkgid, nothing)
1198+
extids === nothing && return
1199+
for extid in extids
1200+
if extid.ntriggers > 0
1201+
# It is possible that pkgid was loaded in an environment
1202+
# below the one of the parent. This will cause a load failure when the
1203+
# pkg ext tries to load the triggers. Therefore, check this first
1204+
# before loading the pkg ext.
1205+
pkgenv = Base.identify_package_env(extid.id, pkgid.name)
1206+
ext_not_allowed_load = false
1207+
if pkgenv === nothing
1208+
ext_not_allowed_load = true
1209+
else
1210+
pkg, env = pkgenv
1211+
path = Base.locate_package(pkg, env)
1212+
if path === nothing
1213+
ext_not_allowed_load = true
1214+
end
1215+
end
1216+
if ext_not_allowed_load
1217+
@debug "Extension $(extid.id.name) of $(extid.parentid.name) will not be loaded \
1218+
since $(pkgid.name) loaded in environment lower in load path"
1219+
# indicate extid is expected to fail
1220+
extid.ntriggers *= -1
1221+
else
1222+
# indicate pkgid is loaded
1223+
extid.ntriggers -= 1
1224+
end
1225+
end
1226+
if extid.ntriggers < 0
1227+
# indicate pkgid is loaded
1228+
extid.ntriggers += 1
1229+
succeeded = false
1230+
else
1231+
succeeded = true
1232+
end
1233+
if extid.ntriggers == 0
1234+
# actually load extid, now that all dependencies are met,
1235+
# and record the result
1236+
succeeded = succeeded && run_extension_callbacks(extid)
1237+
succeeded || push!(EXT_DORMITORY_FAILED, extid)
1238+
end
12151239
end
12161240
nothing
12171241
end
@@ -1224,7 +1248,19 @@ This is used in cases where the automatic loading of an extension failed
12241248
due to some problem with the extension. Instead of restarting the Julia session,
12251249
the extension can be fixed, and this function run.
12261250
"""
1227-
retry_load_extensions() = run_extension_callbacks(; force=true)
1251+
function retry_load_extensions()
1252+
@lock require_lock begin
1253+
# this copy is desired since run_extension_callbacks will release this lock
1254+
# so this can still mutate the list to drop successful ones
1255+
failed = copy(EXT_DORMITORY_FAILED)
1256+
empty!(EXT_DORMITORY_FAILED)
1257+
filter!(failed) do extid
1258+
return !run_extension_callbacks(extid)
1259+
end
1260+
prepend!(EXT_DORMITORY_FAILED, failed)
1261+
end
1262+
nothing
1263+
end
12281264

12291265
"""
12301266
get_extension(parent::Module, extension::Symbol)

0 commit comments

Comments
 (0)