Skip to content

Commit 1b2dc22

Browse files
authored
Add locking to revise() (#846)
Fixes #837, fixes #845
1 parent 891b739 commit 1b2dc22

File tree

2 files changed

+149
-132
lines changed

2 files changed

+149
-132
lines changed

src/packagedef.jl

+101-88
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ Global variable, a set of `Manifest.toml` files from the active projects used du
8585
"""
8686
const watched_manifests = Set{String}()
8787

88+
# Locks access to `revision_queue` to prevent race conditions, see issues #837 and #845
89+
const revise_lock = ReentrantLock()
90+
8891
"""
8992
Revise.revision_queue
9093
@@ -586,9 +589,11 @@ This is generally called via a [`Revise.TaskThunk`](@ref).
586589
end
587590
if id != NOPACKAGE
588591
pkgdata = pkgdatas[id]
589-
if hasfile(pkgdata, key) # issue #228
590-
push!(revision_queue, (pkgdata, relpath(key, pkgdata)))
591-
notify(revision_event)
592+
lock(revise_lock) do
593+
if hasfile(pkgdata, key) # issue #228
594+
push!(revision_queue, (pkgdata, relpath(key, pkgdata)))
595+
notify(revision_event)
596+
end
592597
end
593598
end
594599
end
@@ -639,7 +644,11 @@ function revise_file_queued(pkgdata::PkgData, file)
639644

640645
# Check to see if we're still watching this file
641646
stillwatching = haskey(watched_files, dirfull)
642-
PkgId(pkgdata) != NOPACKAGE && push!(revision_queue, (pkgdata, relpath(file, pkgdata)))
647+
if PkgId(pkgdata) != NOPACKAGE
648+
lock(revise_lock) do
649+
push!(revision_queue, (pkgdata, relpath(file, pkgdata)))
650+
end
651+
end
643652
end
644653
return
645654
end
@@ -734,8 +743,10 @@ end
734743
Attempt to perform previously-failed revisions. This can be useful in cases of order-dependent errors.
735744
"""
736745
function retry()
737-
for (k, v) in queue_errors
738-
push!(revision_queue, k)
746+
lock(revise_lock) do
747+
for (k, v) in queue_errors
748+
push!(revision_queue, k)
749+
end
739750
end
740751
revise()
741752
end
@@ -749,100 +760,102 @@ otherwise these are only logged.
749760
"""
750761
function revise(; throw=false)
751762
sleep(0.01) # in case the file system isn't quite done writing out the new files
752-
have_queue_errors = !isempty(queue_errors)
753-
754-
# Do all the deletion first. This ensures that a method that moved from one file to another
755-
# won't get redefined first and deleted second.
756-
revision_errors = Tuple{PkgData,String}[]
757-
queue = sort!(collect(revision_queue); lt=pkgfileless)
758-
finished = eltype(revision_queue)[]
759-
mexsnews = ModuleExprsSigs[]
760-
interrupt = false
761-
for (pkgdata, file) in queue
762-
try
763-
push!(mexsnews, handle_deletions(pkgdata, file)[1])
764-
push!(finished, (pkgdata, file))
765-
catch err
766-
throw && Base.throw(err)
767-
interrupt |= isa(err, InterruptException)
768-
push!(revision_errors, (pkgdata, file))
769-
queue_errors[(pkgdata, file)] = (err, catch_backtrace())
763+
lock(revise_lock) do
764+
have_queue_errors = !isempty(queue_errors)
765+
766+
# Do all the deletion first. This ensures that a method that moved from one file to another
767+
# won't get redefined first and deleted second.
768+
revision_errors = Tuple{PkgData,String}[]
769+
queue = sort!(collect(revision_queue); lt=pkgfileless)
770+
finished = eltype(revision_queue)[]
771+
mexsnews = ModuleExprsSigs[]
772+
interrupt = false
773+
for (pkgdata, file) in queue
774+
try
775+
push!(mexsnews, handle_deletions(pkgdata, file)[1])
776+
push!(finished, (pkgdata, file))
777+
catch err
778+
throw && Base.throw(err)
779+
interrupt |= isa(err, InterruptException)
780+
push!(revision_errors, (pkgdata, file))
781+
queue_errors[(pkgdata, file)] = (err, catch_backtrace())
782+
end
770783
end
771-
end
772-
# Do the evaluation
773-
for ((pkgdata, file), mexsnew) in zip(finished, mexsnews)
774-
defaultmode = PkgId(pkgdata).name == "Main" ? :evalmeth : :eval
775-
i = fileindex(pkgdata, file)
776-
i === nothing && continue # file was deleted by `handle_deletions`
777-
fi = fileinfo(pkgdata, i)
778-
modsremaining = Set(keys(mexsnew))
779-
changed, err = true, nothing
780-
while changed
781-
changed = false
782-
for (mod, exsnew) in mexsnew
783-
mod modsremaining || continue
784-
try
785-
mode = defaultmode
786-
# Allow packages to override the supplied mode
787-
if isdefined(mod, :__revise_mode__)
788-
mode = getfield(mod, :__revise_mode__)::Symbol
789-
end
790-
mode (:sigs, :eval, :evalmeth, :evalassign) || error("unsupported mode ", mode)
791-
exsold = get(fi.modexsigs, mod, empty_exs_sigs)
792-
for rex in keys(exsnew)
793-
sigs, includes = eval_rex(rex, exsold, mod; mode=mode)
794-
if sigs !== nothing
795-
exsnew[rex] = sigs
784+
# Do the evaluation
785+
for ((pkgdata, file), mexsnew) in zip(finished, mexsnews)
786+
defaultmode = PkgId(pkgdata).name == "Main" ? :evalmeth : :eval
787+
i = fileindex(pkgdata, file)
788+
i === nothing && continue # file was deleted by `handle_deletions`
789+
fi = fileinfo(pkgdata, i)
790+
modsremaining = Set(keys(mexsnew))
791+
changed, err = true, nothing
792+
while changed
793+
changed = false
794+
for (mod, exsnew) in mexsnew
795+
mod modsremaining || continue
796+
try
797+
mode = defaultmode
798+
# Allow packages to override the supplied mode
799+
if isdefined(mod, :__revise_mode__)
800+
mode = getfield(mod, :__revise_mode__)::Symbol
796801
end
797-
if includes !== nothing
798-
maybe_add_includes_to_pkgdata!(pkgdata, file, includes; eval_now=true)
802+
mode (:sigs, :eval, :evalmeth, :evalassign) || error("unsupported mode ", mode)
803+
exsold = get(fi.modexsigs, mod, empty_exs_sigs)
804+
for rex in keys(exsnew)
805+
sigs, includes = eval_rex(rex, exsold, mod; mode=mode)
806+
if sigs !== nothing
807+
exsnew[rex] = sigs
808+
end
809+
if includes !== nothing
810+
maybe_add_includes_to_pkgdata!(pkgdata, file, includes; eval_now=true)
811+
end
799812
end
813+
delete!(modsremaining, mod)
814+
changed = true
815+
catch _err
816+
err = _err
800817
end
801-
delete!(modsremaining, mod)
802-
changed = true
803-
catch _err
804-
err = _err
805818
end
806819
end
820+
if isempty(modsremaining)
821+
pkgdata.fileinfos[i] = FileInfo(mexsnew, fi)
822+
delete!(queue_errors, (pkgdata, file))
823+
else
824+
throw && Base.throw(err)
825+
interrupt |= isa(err, InterruptException)
826+
push!(revision_errors, (pkgdata, file))
827+
queue_errors[(pkgdata, file)] = (err, catch_backtrace())
828+
end
807829
end
808-
if isempty(modsremaining)
809-
pkgdata.fileinfos[i] = FileInfo(mexsnew, fi)
810-
delete!(queue_errors, (pkgdata, file))
830+
if interrupt
831+
for pkgfile in finished
832+
haskey(queue_errors, pkgfile) || delete!(revision_queue, pkgfile)
833+
end
811834
else
812-
throw && Base.throw(err)
813-
interrupt |= isa(err, InterruptException)
814-
push!(revision_errors, (pkgdata, file))
815-
queue_errors[(pkgdata, file)] = (err, catch_backtrace())
835+
empty!(revision_queue)
816836
end
817-
end
818-
if interrupt
819-
for pkgfile in finished
820-
haskey(queue_errors, pkgfile) || delete!(revision_queue, pkgfile)
821-
end
822-
else
823-
empty!(revision_queue)
824-
end
825-
errors(revision_errors)
826-
if !isempty(queue_errors)
827-
if !have_queue_errors # only print on the first time errors occur
828-
io = IOBuffer()
829-
println(io, "\n") # better here than in the triple-quoted literal, see https://github.com/JuliaLang/julia/issues/34105
830-
for (pkgdata, file) in keys(queue_errors)
831-
println(io, " ", joinpath(basedir(pkgdata), file))
837+
errors(revision_errors)
838+
if !isempty(queue_errors)
839+
if !have_queue_errors # only print on the first time errors occur
840+
io = IOBuffer()
841+
println(io, "\n") # better here than in the triple-quoted literal, see https://github.com/JuliaLang/julia/issues/34105
842+
for (pkgdata, file) in keys(queue_errors)
843+
println(io, " ", joinpath(basedir(pkgdata), file))
844+
end
845+
str = String(take!(io))
846+
@warn """The running code does not match the saved version for the following files:$str
847+
If the error was due to evaluation order, it can sometimes be resolved by calling `Revise.retry()`.
848+
Use Revise.errors() to report errors again. Only the first error in each file is shown.
849+
Your prompt color may be yellow until the errors are resolved."""
850+
maybe_set_prompt_color(:warn)
832851
end
833-
str = String(take!(io))
834-
@warn """The running code does not match the saved version for the following files:$str
835-
If the error was due to evaluation order, it can sometimes be resolved by calling `Revise.retry()`.
836-
Use Revise.errors() to report errors again. Only the first error in each file is shown.
837-
Your prompt color may be yellow until the errors are resolved."""
838-
maybe_set_prompt_color(:warn)
852+
else
853+
maybe_set_prompt_color(:ok)
839854
end
840-
else
841-
maybe_set_prompt_color(:ok)
842-
end
843-
tracking_Main_includes[] && queue_includes(Main)
855+
tracking_Main_includes[] && queue_includes(Main)
844856

845-
process_user_callbacks!(throw=throw)
857+
process_user_callbacks!(throw=throw)
858+
end
846859

847860
nothing
848861
end

src/recipes.jl

+48-44
Original file line numberDiff line numberDiff line change
@@ -61,22 +61,24 @@ function _track(id, modname; modified_files=revision_queue)
6161
if pkgdata === nothing
6262
pkgdata = PkgData(id, srcdir)
6363
end
64-
for (submod, filename) in Iterators.drop(Base._included_files, 1) # stepping through sysimg.jl rebuilds Base, omit it
65-
ffilename = fixpath(filename)
66-
inpath(ffilename, dirs) || continue
67-
keypath = ffilename[1:last(findfirst(dirs[end], ffilename))]
68-
rpath = relpath(ffilename, keypath)
69-
fullpath = joinpath(basedir(pkgdata), rpath)
70-
if fullpath != filename
71-
cache_file_key[fullpath] = filename
72-
src_file_key[filename] = fullpath
73-
end
74-
push!(pkgdata, rpath=>FileInfo(submod, basesrccache))
75-
if mtime(ffilename) > mtcache
76-
with_logger(_debug_logger) do
77-
@debug "Recipe for Base/StdLib" _group="Watching" filename=filename mtime=mtime(filename) mtimeref=mtcache
64+
lock(revise_lock) do
65+
for (submod, filename) in Iterators.drop(Base._included_files, 1) # stepping through sysimg.jl rebuilds Base, omit it
66+
ffilename = fixpath(filename)
67+
inpath(ffilename, dirs) || continue
68+
keypath = ffilename[1:last(findfirst(dirs[end], ffilename))]
69+
rpath = relpath(ffilename, keypath)
70+
fullpath = joinpath(basedir(pkgdata), rpath)
71+
if fullpath != filename
72+
cache_file_key[fullpath] = filename
73+
src_file_key[filename] = fullpath
74+
end
75+
push!(pkgdata, rpath=>FileInfo(submod, basesrccache))
76+
if mtime(ffilename) > mtcache
77+
with_logger(_debug_logger) do
78+
@debug "Recipe for Base/StdLib" _group="Watching" filename=filename mtime=mtime(filename) mtimeref=mtcache
79+
end
80+
push!(modified_files, (pkgdata, rpath))
7881
end
79-
push!(modified_files, (pkgdata, rpath))
8082
end
8183
end
8284
# Add files to CodeTracking pkgfiles
@@ -135,39 +137,41 @@ function track_subdir_from_git!(pkgdata::PkgData, subdir::AbstractString; commit
135137
tree = git_tree(repo, commit)
136138
files = Iterators.filter(file->startswith(file, prefix) && endswith(file, ".jl"), keys(tree))
137139
ccall((:giterr_clear, :libgit2), Cvoid, ()) # necessary to avoid errors like "the global/xdg file 'attributes' doesn't exist: No such file or directory"
138-
for file in files
139-
fullpath = joinpath(repo_path, file)
140-
rpath = relpath(fullpath, pkgdata) # this might undo the above, except for Core.Compiler
141-
local src
142-
try
143-
src = git_source(file, tree)
144-
catch err
145-
if err isa KeyError
146-
@warn "skipping $file, not found in repo"
147-
continue
140+
lock(revise_lock) do
141+
for file in files
142+
fullpath = joinpath(repo_path, file)
143+
rpath = relpath(fullpath, pkgdata) # this might undo the above, except for Core.Compiler
144+
local src
145+
try
146+
src = git_source(file, tree)
147+
catch err
148+
if err isa KeyError
149+
@warn "skipping $file, not found in repo"
150+
continue
151+
end
152+
rethrow(err)
148153
end
149-
rethrow(err)
150-
end
151-
fmod = get(juliaf2m, fullpath, Core.Compiler) # Core.Compiler is not cached
152-
if fmod === Core.Compiler
153-
endswith(fullpath, "compiler.jl") && continue # defines the module, skip
154-
@static if isdefined(Core.Compiler, :EscapeAnalysis)
155-
# after https://github.com/JuliaLang/julia/pull/43800
156-
if contains(fullpath, "compiler/ssair/EscapeAnalysis")
157-
fmod = Core.Compiler.EscapeAnalysis
154+
fmod = get(juliaf2m, fullpath, Core.Compiler) # Core.Compiler is not cached
155+
if fmod === Core.Compiler
156+
endswith(fullpath, "compiler.jl") && continue # defines the module, skip
157+
@static if isdefined(Core.Compiler, :EscapeAnalysis)
158+
# after https://github.com/JuliaLang/julia/pull/43800
159+
if contains(fullpath, "compiler/ssair/EscapeAnalysis")
160+
fmod = Core.Compiler.EscapeAnalysis
161+
end
158162
end
159163
end
164+
if src != read(fullpath, String)
165+
push!(modified_files, (pkgdata, rpath))
166+
end
167+
fi = FileInfo(fmod)
168+
if parse_source!(fi.modexsigs, src, file, fmod) === nothing
169+
@warn "failed to parse Git source text for $file"
170+
else
171+
instantiate_sigs!(fi.modexsigs)
172+
end
173+
push!(pkgdata, rpath=>fi)
160174
end
161-
if src != read(fullpath, String)
162-
push!(modified_files, (pkgdata, rpath))
163-
end
164-
fi = FileInfo(fmod)
165-
if parse_source!(fi.modexsigs, src, file, fmod) === nothing
166-
@warn "failed to parse Git source text for $file"
167-
else
168-
instantiate_sigs!(fi.modexsigs)
169-
end
170-
push!(pkgdata, rpath=>fi)
171175
end
172176
if !isempty(pkgdata.fileinfos)
173177
id = PkgId(pkgdata)

0 commit comments

Comments
 (0)