Skip to content

Commit 1927a41

Browse files
committed
Add persistent_tasks test
1 parent 8dc359d commit 1927a41

File tree

10 files changed

+220
-4
lines changed

10 files changed

+220
-4
lines changed

docs/src/index.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Aqua.jl:
1+
# Aqua.jl:
22
## *A*uto *QU*ality *A*ssurance for Julia packages
33

44
Aqua.jl provides functions to run a few automatable checks for Julia packages:
@@ -13,6 +13,7 @@ Aqua.jl provides functions to run a few automatable checks for Julia packages:
1313
`compat` entry.
1414
* `Project.toml` formatting is compatible with Pkg.jl output.
1515
* There are no "obvious" type piracies.
16+
* The package does not create any persistent Tasks that might block precompilation of dependencies.
1617

1718
## Quick usage
1819

@@ -55,14 +56,14 @@ recommended to add a version bound for Aqua.jl.
5556
test = ["Aqua", "Test"]
5657
```
5758

58-
If your package supports Julia pre-1.2, you need to use the second approach,
59+
If your package supports Julia pre-1.2, you need to use the second approach,
5960
although you can use both approaches at the same time.
6061

6162
!!! warning
6263
In normal use, `Aqua.jl` should not be added to `[deps]` in `YourPackage/Project.toml`!
6364

6465
### ...to your tests?
65-
It is recommended to create a separate file `YourPackage/test/Aqua.jl` that gets included in `YourPackage/test/runtests.jl`
66+
It is recommended to create a separate file `YourPackage/test/Aqua.jl` that gets included in `YourPackage/test/runtests.jl`
6667
with either
6768

6869
```julia

src/Aqua.jl

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module Aqua
22

33
using Base: PkgId, UUID
4-
using Pkg: Pkg, TOML
4+
using Pkg: Pkg, TOML, PackageSpec
55
using Test
66

77
try
@@ -22,6 +22,7 @@ include("stale_deps.jl")
2222
include("deps_compat.jl")
2323
include("project_toml_formatting.jl")
2424
include("piracy.jl")
25+
include("persistent_tasks.jl")
2526

2627
"""
2728
test_all(testtarget::Module)
@@ -36,6 +37,7 @@ Run following tests in isolated testset:
3637
* [`test_deps_compat(testtarget)`](@ref test_deps_compat)
3738
* [`test_project_toml_formatting(testtarget)`](@ref test_project_toml_formatting)
3839
* [`test_piracy(testtarget)`](@ref test_piracy)
40+
* [`test_persistent_tasks(testtarget)`](@ref test_persistent_tasks)
3941
4042
The keyword argument `\$x` (e.g., `ambiguities`) can be used to
4143
control whether or not to run `test_\$x` (e.g., `test_ambiguities`).
@@ -51,6 +53,7 @@ passed to `\$x` to specify the keyword arguments for `test_\$x`.
5153
- `deps_compat = true`
5254
- `project_toml_formatting = true`
5355
- `piracy = true`
56+
- `persistent_tasks = true`
5457
"""
5558
function test_all(
5659
testtarget::Module;
@@ -62,6 +65,7 @@ function test_all(
6265
deps_compat = true,
6366
project_toml_formatting = true,
6467
piracy = true,
68+
persistent_tasks = true,
6569
)
6670
@testset "Method ambiguity" begin
6771
if ambiguities !== false
@@ -103,6 +107,11 @@ function test_all(
103107
test_piracy(testtarget; askwargs(piracy)...)
104108
end
105109
end
110+
@testset "Persistent tasks" begin
111+
if persistent_tasks !== false
112+
test_persistent_tasks(testtarget; askwargs(persistent_tasks)...)
113+
end
114+
end
106115
end
107116

108117
end # module

src/persistent_tasks.jl

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""
2+
Aqua.test_persistent_tasks(package; tmax=5)
3+
4+
Test whether loading `package` creates persistent `Task`s
5+
which may block precompilation of dependent packages.
6+
7+
# Motivation
8+
9+
Julia 1.10 and higher wait for all running `Task`s to finish
10+
before writing out the precompiled (cached) version of the package.
11+
One consequence is that a package that launches
12+
`Task`s in its `__init__` function may precompile successfully,
13+
but block precompilation of any packages that depend on it.
14+
15+
# Example
16+
17+
Let's create a dummy package, `PkgA`, that launches a persistent `Task`:
18+
19+
```julia
20+
21+
```julia
22+
module PkgA
23+
const t = Ref{Any}() # to prevent the Timer from being garbage-collected
24+
__init__() = t[] = Timer(0.1; interval=1) # create a persistent `Timer` `Task`
25+
end
26+
```
27+
28+
`PkgA` will precompile successfully, because `PkgA.__init__()` does not
29+
run when `PkgA` is precompiled. However,
30+
31+
```julia
32+
module PkgB
33+
using PkgA
34+
end
35+
```
36+
37+
fails to precompile: `using PkgA` runs `PkgA.__init__()`, which
38+
leaves the `Timer` `Task` running, and that causes precompilation
39+
of `PkgB` to hang.
40+
41+
# How the test works
42+
43+
This test works by launching a Julia process that tries to precompile a
44+
dummy package similar to `PkgB` above, modified to signal back to Aqua when
45+
`PkgA` has finished loading. The test fails if the gap between loading `PkgA`
46+
and finishing precompilation exceeds time `tmax`.
47+
48+
# How to fix failing packages
49+
50+
Often, the easiest fix is to modify the `__init__` function to check whether the
51+
Julia process is precompiling some other package; if so, don't launch the
52+
persistent `Task`s.
53+
54+
```
55+
function __init__()
56+
# Other setup code here
57+
if ccall(:jl_generating_output, Cint, ()) == 0 # if we're not precompiling...
58+
# launch persistent tasks here
59+
end
60+
end
61+
```
62+
63+
In more complex cases, you may need to set up independently-callable functions
64+
to launch the tasks and cleanly shut them down.
65+
66+
# Arguments
67+
- `package`: a top-level `Module` or `Base.PkgId`.
68+
69+
# Keyword Arguments
70+
- `tmax::Real`: the maximum time (in seconds) to wait between loading the
71+
package and forced shutdown of the precompilation process.
72+
"""
73+
function test_persistent_tasks(package::PkgId; tmax=5, fails::Bool=false)
74+
@testset "$package persistent_tasks" begin
75+
result = root_project_or_failed_lazytest(package)
76+
result isa LazyTestResult && return result
77+
@test fails precompile_wrapper(result, tmax)
78+
end
79+
end
80+
81+
function test_persistent_tasks(package::Module; kwargs...)
82+
test_persistent_tasks(PkgId(package); kwargs...)
83+
end
84+
85+
"""
86+
Aqua.test_persistent_tasks_deps(package; kwargs...)
87+
88+
Test all the dependencies of `package` with [`Aqua.test_persistent_tasks`](@ref).
89+
"""
90+
function test_persistent_tasks_deps(package::PkgId; fails=Dict{String,Bool}(), kwargs...)
91+
result = root_project_or_failed_lazytest(package)
92+
result isa LazyTestResult && return result
93+
prj = TOML.parsefile(result)
94+
deps = get(prj, "deps", nothing)
95+
@testset "$result" begin
96+
if deps === nothing
97+
return LazyTestResult("$package", "`$result` does not have `deps`", true)
98+
else
99+
for (name, uuid) in deps
100+
id = PkgId(UUID(uuid), name)
101+
test_persistent_tasks(id; fails=get(fails, name, false), kwargs...)
102+
end
103+
end
104+
end
105+
end
106+
107+
function test_persistent_tasks_deps(package::Module; kwargs...)
108+
test_persistent_tasks_deps(PkgId(package); kwargs...)
109+
end
110+
111+
function precompile_wrapper(project, tmax)
112+
pkgdir = dirname(project)
113+
pkgname = basename(pkgdir)
114+
wrapperdir = tempname()
115+
wrappername, wrapperuuid = only(Pkg.generate(wrapperdir))
116+
Pkg.activate(wrapperdir)
117+
Pkg.develop(PackageSpec(path=pkgdir))
118+
open(joinpath(wrapperdir, "src", wrappername * ".jl"), "w") do io
119+
println(io, """
120+
module $wrappername
121+
using $pkgname
122+
# Signal Aqua from the precompilation process that we've finished loading the package
123+
open(joinpath("$wrapperdir", "done.log"), "w") do io
124+
println(io, "done")
125+
end
126+
end
127+
""")
128+
end
129+
# Precompile the wrapper package
130+
cmd = `$(Base.julia_cmd()) --project=$wrapperdir -e 'using Pkg; Pkg.precompile()'`
131+
proc = run(cmd; wait=false)
132+
while !isfile(joinpath(wrapperdir, "done.log"))
133+
sleep(0.1)
134+
end
135+
# Check whether precompilation finishes in the required time
136+
t = time()
137+
while process_running(proc) && time() - t < tmax
138+
sleep(0.1)
139+
end
140+
success = !process_running(proc)
141+
if !success
142+
kill(proc)
143+
end
144+
return success
145+
end
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
name = "PersistentTask"
2+
uuid = "e5c298b6-d81d-47aa-a9ed-5ea983e22986"
3+
authors = ["Tim Holy <[email protected]>"]
4+
version = "0.1.0"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module PersistentTask
2+
3+
const t = Ref{Any}()
4+
__init__() = t[] = Timer(0.1; interval=1) # create a persistent `Timer` `Task`
5+
6+
end # module PersistentTask
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
name = "TransientTask"
2+
uuid = "94ae9332-58b0-4342-989c-0a7e44abcca9"
3+
authors = ["Tim Holy <[email protected]>"]
4+
version = "0.1.0"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module TransientTask
2+
3+
__init__() = Timer(0.1) # create a transient `Timer` `Task`
4+
5+
end # module TransientTask
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name = "UsesBoth"
2+
uuid = "96f12b6e-60f8-43dc-b131-049a88a2f499"
3+
authors = ["Tim Holy <[email protected]>"]
4+
version = "0.1.0"
5+
6+
[deps]
7+
PersistentTask = "e5c298b6-d81d-47aa-a9ed-5ea983e22986"
8+
TransientTask = "94ae9332-58b0-4342-989c-0a7e44abcca9"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module UsesBoth
2+
3+
using TransientTask
4+
using PersistentTask
5+
6+
end # module UsesBoth

test/test_persistent_tasks.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module TestPersistentTasks
2+
3+
include("preamble.jl")
4+
using Base: PkgId, UUID
5+
using Pkg: TOML
6+
7+
function getid(name)
8+
path = joinpath(@__DIR__, "pkgs", "PersistentTasks", name)
9+
if path LOAD_PATH
10+
pushfirst!(LOAD_PATH, path)
11+
end
12+
prj = TOML.parsefile(joinpath(path, "Project.toml"))
13+
return PkgId(UUID(prj["uuid"]), prj["name"])
14+
end
15+
16+
17+
@testset "PersistentTasks" begin
18+
Aqua.test_persistent_tasks(getid("TransientTask"))
19+
Aqua.test_persistent_tasks_deps(getid("TransientTask"))
20+
21+
if all((Base.VERSION.major, Base.VERSION.minor) .>= (1, 10))
22+
Aqua.test_persistent_tasks(getid("PersistentTask"); fails=true)
23+
Aqua.test_persistent_tasks_deps(getid("UsesBoth"); fails=Dict("PersistentTask" => true))
24+
end
25+
filter!(str -> !occursin("PersistentTasks", str), LOAD_PATH)
26+
end
27+
28+
end

0 commit comments

Comments
 (0)