Skip to content

Commit b2d612c

Browse files
timholyfingolfinlgoettgens
authored
Add persistent_tasks test (#174)
* Add persistent_tasks test * Run JuliaFormatter, response to review comments Co-authored-by: "Lars Göttgens <[email protected]>" * Improve robustness Flushing the io may prevent some of the CI hangs; also print diagnostics in case of unexpected outcomes. * More JuliaFormatter * Escape paths (windows) * Update src/persistent_tasks.jl Co-authored-by: Max Horn <[email protected]> * fails -> broken * Add to CHANGELOG * Adapt interface * Update changelog * Activate new test in `test_smoke` * Update src/persistent_tasks.jl Co-authored-by: Max Horn <[email protected]> * Restore the previous environment * Apply suggestions from code review Co-authored-by: Lars Göttgens <[email protected]> * Turn `test_persistent_tasks` on by default --------- Co-authored-by: Max Horn <[email protected]> Co-authored-by: Lars Göttgens <[email protected]> Co-authored-by: Lars Göttgens <[email protected]>
1 parent 44c89fd commit b2d612c

File tree

12 files changed

+251
-4
lines changed

12 files changed

+251
-4
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Two additions check whether packages might block precompilation on Julia 1.10 or higher: ([#174](https://github.com/JuliaTesting/Aqua.jl/pull/174))
13+
+ `test_persistent_tasks` tests whether "your" package can safely be used as a dependency for downstream packages. This test is enabled for the default testsuite, but you can opt-out by supplying `persistent_tasks=false` to `test_all`. [BREAKING]
14+
+ `find_persistent_tasks_deps` is useful if "your" package hangs upon precompilation: it runs `test_persistent_tasks` on all the things you depend on, and may help isolate the culprit(s).
15+
1016
### Changed
1117

1218
- The minimum requirement for julia was raised from `1.0` to `1.4`. ([#221](https://github.com/JuliaTesting/Aqua.jl/pull/221))

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
@static if VERSION >= v"1.7-"
@@ -21,6 +21,7 @@ include("stale_deps.jl")
2121
include("deps_compat.jl")
2222
include("project_toml_formatting.jl")
2323
include("piracy.jl")
24+
include("persistent_tasks.jl")
2425

2526
"""
2627
test_all(testtarget::Module)
@@ -35,6 +36,7 @@ Run following tests in isolated testset:
3536
* [`test_deps_compat(testtarget)`](@ref test_deps_compat)
3637
* [`test_project_toml_formatting(testtarget)`](@ref test_project_toml_formatting)
3738
* [`test_piracy(testtarget)`](@ref test_piracy)
39+
* [`test_persistent_tasks(testtarget)`](@ref test_persistent_tasks)
3840
3941
The keyword argument `\$x` (e.g., `ambiguities`) can be used to
4042
control whether or not to run `test_\$x` (e.g., `test_ambiguities`).
@@ -50,6 +52,7 @@ passed to `\$x` to specify the keyword arguments for `test_\$x`.
5052
- `deps_compat = true`
5153
- `project_toml_formatting = true`
5254
- `piracy = true`
55+
- `persistent_tasks = true`
5356
"""
5457
function test_all(
5558
testtarget::Module;
@@ -61,6 +64,7 @@ function test_all(
6164
deps_compat = true,
6265
project_toml_formatting = true,
6366
piracy = true,
67+
persistent_tasks = true,
6468
)
6569
@testset "Method ambiguity" begin
6670
if ambiguities !== false
@@ -102,6 +106,11 @@ function test_all(
102106
test_piracy(testtarget; askwargs(piracy)...)
103107
end
104108
end
109+
@testset "Persistent tasks" begin
110+
if persistent_tasks !== false
111+
test_persistent_tasks(testtarget; askwargs(persistent_tasks)...)
112+
end
113+
end
105114
end
106115

107116
end # module

src/persistent_tasks.jl

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"""
2+
Aqua.test_persistent_tasks(package)
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+
module PkgA
21+
const t = Ref{Any}() # to prevent the Timer from being garbage-collected
22+
__init__() = t[] = Timer(0.1; interval=1) # create a persistent `Timer` `Task`
23+
end
24+
```
25+
26+
`PkgA` will precompile successfully, because `PkgA.__init__()` does not
27+
run when `PkgA` is precompiled. However,
28+
29+
```julia
30+
module PkgB
31+
using PkgA
32+
end
33+
```
34+
35+
fails to precompile: `using PkgA` runs `PkgA.__init__()`, which
36+
leaves the `Timer` `Task` running, and that causes precompilation
37+
of `PkgB` to hang.
38+
39+
# How the test works
40+
41+
This test works by launching a Julia process that tries to precompile a
42+
dummy package similar to `PkgB` above, modified to signal back to Aqua when
43+
`PkgA` has finished loading. The test fails if the gap between loading `PkgA`
44+
and finishing precompilation exceeds time `tmax`.
45+
46+
# How to fix failing packages
47+
48+
Often, the easiest fix is to modify the `__init__` function to check whether the
49+
Julia process is precompiling some other package; if so, don't launch the
50+
persistent `Task`s.
51+
52+
```
53+
function __init__()
54+
# Other setup code here
55+
if ccall(:jl_generating_output, Cint, ()) == 0 # if we're not precompiling...
56+
# launch persistent tasks here
57+
end
58+
end
59+
```
60+
61+
In more complex cases, you may need to set up independently-callable functions
62+
to launch the tasks and set conditions that allow them to cleanly exit.
63+
64+
# Arguments
65+
- `package`: a top-level `Module` or `Base.PkgId`.
66+
67+
# Keyword Arguments
68+
- `broken::Bool = false`: If true, it uses `@test_broken` instead of
69+
`@test`.
70+
- `tmax::Real = 5`: the maximum time (in seconds) to wait after loading the
71+
package before forcibly shutting down the precompilation process (triggering
72+
a test failure).
73+
"""
74+
function test_persistent_tasks(package::PkgId; broken::Bool = false, kwargs...)
75+
if broken
76+
@test_broken !has_persistent_tasks(package; kwargs...)
77+
else
78+
@test !has_persistent_tasks(package; kwargs...)
79+
end
80+
end
81+
82+
function test_persistent_tasks(package::Module; kwargs...)
83+
test_persistent_tasks(PkgId(package); kwargs...)
84+
end
85+
86+
function has_persistent_tasks(package::PkgId; tmax = 10)
87+
result = root_project_or_failed_lazytest(package)
88+
result isa LazyTestResult && error("Unable to locate Project.toml")
89+
return !precompile_wrapper(result, tmax)
90+
end
91+
92+
"""
93+
Aqua.find_persistent_tasks_deps(package; broken = Dict{String,Bool}(), kwargs...)
94+
95+
Test all the dependencies of `package` with [`Aqua.test_persistent_tasks`](@ref).
96+
On Julia 1.10 and higher, it returns a list of all dependencies failing the test.
97+
These are likely the ones blocking precompilation of your package.
98+
99+
Any additional kwargs (e.g., `tmax`) are passed to [`Aqua.test_persistent_tasks`](@ref).
100+
"""
101+
function find_persistent_tasks_deps(package::PkgId; kwargs...)
102+
result = root_project_or_failed_lazytest(package)
103+
result isa LazyTestResult && error("Unable to locate Project.toml")
104+
prj = TOML.parsefile(result)
105+
deps = get(prj, "deps", Dict{String,Any}())
106+
filter!(deps) do (name, uuid)
107+
id = PkgId(UUID(uuid), name)
108+
return has_persistent_tasks(id; kwargs...)
109+
end
110+
return [name for (name, _) in deps]
111+
end
112+
113+
function find_persistent_tasks_deps(package::Module; kwargs...)
114+
find_persistent_tasks_deps(PkgId(package); kwargs...)
115+
end
116+
117+
function precompile_wrapper(project, tmax)
118+
prev_project = Base.active_project()
119+
isdefined(Pkg, :respect_sysimage_versions) && Pkg.respect_sysimage_versions(false)
120+
try
121+
pkgdir = dirname(project)
122+
pkgname = get(TOML.parsefile(project), "name", nothing)
123+
if isnothing(pkgname)
124+
@error "Unable to locate package name in $project"
125+
return false
126+
end
127+
wrapperdir = tempname()
128+
wrappername, _ = only(Pkg.generate(wrapperdir))
129+
Pkg.activate(wrapperdir)
130+
Pkg.develop(PackageSpec(path = pkgdir))
131+
statusfile = joinpath(wrapperdir, "done.log")
132+
open(joinpath(wrapperdir, "src", wrappername * ".jl"), "w") do io
133+
println(
134+
io,
135+
"""
136+
module $wrappername
137+
using $pkgname
138+
# Signal Aqua from the precompilation process that we've finished loading the package
139+
open("$(escape_string(statusfile))", "w") do io
140+
println(io, "done")
141+
flush(io)
142+
end
143+
end
144+
""",
145+
)
146+
end
147+
# Precompile the wrapper package
148+
cmd = `$(Base.julia_cmd()) --project=$wrapperdir -e 'using Pkg; Pkg.precompile()'`
149+
proc = run(cmd; wait = false)
150+
while !isfile(statusfile) && process_running(proc)
151+
sleep(0.5)
152+
end
153+
if !isfile(statusfile)
154+
@error "Unexpected error: $statusfile was not created, but precompilation exited"
155+
return false
156+
end
157+
# Check whether precompilation finishes in the required time
158+
t = time()
159+
while process_running(proc) && time() - t < tmax
160+
sleep(0.1)
161+
end
162+
success = !process_running(proc)
163+
if !success
164+
kill(proc)
165+
end
166+
return success
167+
finally
168+
isdefined(Pkg, :respect_sysimage_versions) && Pkg.respect_sysimage_versions(true)
169+
Pkg.activate(prev_project)
170+
end
171+
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
name = "PersistentTask"
2+
uuid = "e5c298b6-d81d-47aa-a9ed-5ea983e22986"
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
name = "TransientTask"
2+
uuid = "94ae9332-58b0-4342-989c-0a7e44abcca9"
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name = "UsesBoth"
2+
uuid = "96f12b6e-60f8-43dc-b131-049a88a2f499"
3+
4+
[deps]
5+
PersistentTask = "e5c298b6-d81d-47aa-a9ed-5ea983e22986"
6+
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

0 commit comments

Comments
 (0)