-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
220 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
""" | ||
Aqua.test_persistent_tasks(package; tmax=5) | ||
Test whether loading `package` creates persistent `Task`s | ||
which may block precompilation of dependent packages. | ||
# Motivation | ||
Julia 1.10 and higher wait for all running `Task`s to finish | ||
before writing out the precompiled (cached) version of the package. | ||
One consequence is that a package that launches | ||
`Task`s in its `__init__` function may precompile successfully, | ||
but block precompilation of any packages that depend on it. | ||
# Example | ||
Let's create a dummy package, `PkgA`, that launches a persistent `Task`: | ||
```julia | ||
```julia | ||
module PkgA | ||
const t = Ref{Any}() # to prevent the Timer from being garbage-collected | ||
__init__() = t[] = Timer(0.1; interval=1) # create a persistent `Timer` `Task` | ||
end | ||
``` | ||
`PkgA` will precompile successfully, because `PkgA.__init__()` does not | ||
run when `PkgA` is precompiled. However, | ||
```julia | ||
module PkgB | ||
using PkgA | ||
end | ||
``` | ||
fails to precompile: `using PkgA` runs `PkgA.__init__()`, which | ||
leaves the `Timer` `Task` running, and that causes precompilation | ||
of `PkgB` to hang. | ||
# How the test works | ||
This test works by launching a Julia process that tries to precompile a | ||
dummy package similar to `PkgB` above, modified to signal back to Aqua when | ||
`PkgA` has finished loading. The test fails if the gap between loading `PkgA` | ||
and finishing precompilation exceeds time `tmax`. | ||
# How to fix failing packages | ||
Often, the easiest fix is to modify the `__init__` function to check whether the | ||
Julia process is precompiling some other package; if so, don't launch the | ||
persistent `Task`s. | ||
``` | ||
function __init__() | ||
# Other setup code here | ||
if ccall(:jl_generating_output, Cint, ()) == 0 # if we're not precompiling... | ||
# launch persistent tasks here | ||
end | ||
end | ||
``` | ||
In more complex cases, you may need to set up independently-callable functions | ||
to launch the tasks and cleanly shut them down. | ||
# Arguments | ||
- `package`: a top-level `Module` or `Base.PkgId`. | ||
# Keyword Arguments | ||
- `tmax::Real`: the maximum time (in seconds) to wait between loading the | ||
package and forced shutdown of the precompilation process. | ||
""" | ||
function test_persistent_tasks(package::PkgId; tmax=5, fails::Bool=false) | ||
@testset "$package persistent_tasks" begin | ||
result = root_project_or_failed_lazytest(package) | ||
result isa LazyTestResult && return result | ||
@test fails ⊻ precompile_wrapper(result, tmax) | ||
end | ||
end | ||
|
||
function test_persistent_tasks(package::Module; kwargs...) | ||
test_persistent_tasks(PkgId(package); kwargs...) | ||
end | ||
|
||
""" | ||
Aqua.test_persistent_tasks_deps(package; kwargs...) | ||
Test all the dependencies of `package` with [`Aqua.test_persistent_tasks`](@ref). | ||
""" | ||
function test_persistent_tasks_deps(package::PkgId; fails=Dict{String,Bool}(), kwargs...) | ||
result = root_project_or_failed_lazytest(package) | ||
result isa LazyTestResult && return result | ||
prj = TOML.parsefile(result) | ||
deps = get(prj, "deps", nothing) | ||
@testset "$result" begin | ||
if deps === nothing | ||
return LazyTestResult("$package", "`$result` does not have `deps`", true) | ||
else | ||
for (name, uuid) in deps | ||
id = PkgId(UUID(uuid), name) | ||
test_persistent_tasks(id; fails=get(fails, name, false), kwargs...) | ||
end | ||
end | ||
end | ||
end | ||
|
||
function test_persistent_tasks_deps(package::Module; kwargs...) | ||
test_persistent_tasks_deps(PkgId(package); kwargs...) | ||
end | ||
|
||
function precompile_wrapper(project, tmax) | ||
pkgdir = dirname(project) | ||
pkgname = basename(pkgdir) | ||
wrapperdir = tempname() | ||
wrappername, wrapperuuid = only(Pkg.generate(wrapperdir)) | ||
Pkg.activate(wrapperdir) | ||
Pkg.develop(PackageSpec(path=pkgdir)) | ||
open(joinpath(wrapperdir, "src", wrappername * ".jl"), "w") do io | ||
println(io, """ | ||
module $wrappername | ||
using $pkgname | ||
# Signal Aqua from the precompilation process that we've finished loading the package | ||
open(joinpath("$wrapperdir", "done.log"), "w") do io | ||
println(io, "done") | ||
end | ||
end | ||
""") | ||
end | ||
# Precompile the wrapper package | ||
cmd = `$(Base.julia_cmd()) --project=$wrapperdir -e 'using Pkg; Pkg.precompile()'` | ||
proc = run(cmd; wait=false) | ||
while !isfile(joinpath(wrapperdir, "done.log")) | ||
sleep(0.1) | ||
end | ||
# Check whether precompilation finishes in the required time | ||
t = time() | ||
while process_running(proc) && time() - t < tmax | ||
sleep(0.1) | ||
end | ||
success = !process_running(proc) | ||
if !success | ||
kill(proc) | ||
end | ||
return success | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
name = "PersistentTask" | ||
uuid = "e5c298b6-d81d-47aa-a9ed-5ea983e22986" | ||
authors = ["Tim Holy <[email protected]>"] | ||
version = "0.1.0" |
6 changes: 6 additions & 0 deletions
6
test/pkgs/PersistentTasks/PersistentTask/src/PersistentTask.jl
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
module PersistentTask | ||
|
||
const t = Ref{Any}() | ||
__init__() = t[] = Timer(0.1; interval=1) # create a persistent `Timer` `Task` | ||
|
||
end # module PersistentTask |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
name = "TransientTask" | ||
uuid = "94ae9332-58b0-4342-989c-0a7e44abcca9" | ||
authors = ["Tim Holy <[email protected]>"] | ||
version = "0.1.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module TransientTask | ||
|
||
__init__() = Timer(0.1) # create a transient `Timer` `Task` | ||
|
||
end # module TransientTask |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
name = "UsesBoth" | ||
uuid = "96f12b6e-60f8-43dc-b131-049a88a2f499" | ||
authors = ["Tim Holy <[email protected]>"] | ||
version = "0.1.0" | ||
|
||
[deps] | ||
PersistentTask = "e5c298b6-d81d-47aa-a9ed-5ea983e22986" | ||
TransientTask = "94ae9332-58b0-4342-989c-0a7e44abcca9" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
module UsesBoth | ||
|
||
using TransientTask | ||
using PersistentTask | ||
|
||
end # module UsesBoth |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
module TestPersistentTasks | ||
|
||
include("preamble.jl") | ||
using Base: PkgId, UUID | ||
using Pkg: TOML | ||
|
||
function getid(name) | ||
path = joinpath(@__DIR__, "pkgs", "PersistentTasks", name) | ||
if path ∉ LOAD_PATH | ||
pushfirst!(LOAD_PATH, path) | ||
end | ||
prj = TOML.parsefile(joinpath(path, "Project.toml")) | ||
return PkgId(UUID(prj["uuid"]), prj["name"]) | ||
end | ||
|
||
|
||
@testset "PersistentTasks" begin | ||
Aqua.test_persistent_tasks(getid("TransientTask")) | ||
Aqua.test_persistent_tasks_deps(getid("TransientTask")) | ||
|
||
if all((Base.VERSION.major, Base.VERSION.minor) .>= (1, 10)) | ||
Aqua.test_persistent_tasks(getid("PersistentTask"); fails=true) | ||
Aqua.test_persistent_tasks_deps(getid("UsesBoth"); fails=Dict("PersistentTask" => true)) | ||
end | ||
filter!(str -> !occursin("PersistentTasks", str), LOAD_PATH) | ||
end | ||
|
||
end |