Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ New library features
along with the type of the entries in a vector of new `DirEntry` objects to provide more efficient `isfile`
etc. checks. `readdir(::DirEntry)` accepts a `DirEntry` as input and, like `readdir(::AbstractString)`,
returns a `Vector{String}` of names. `DirEntry` is exported from `Base` ([#55358]).
* Calls to `wait` on one-shot `Timer`s that have already triggered no longer throw `EOFError`. Previously
only the first `wait` returned and subsequent `wait` calls would throw ([#62539])
* When the display height is too small to show any array entries, the `text/plain` array display
(used e.g. by the REPL and when logging values with `@info` etc.) now shows as many entries as
fit on a single line, truncated to the display width, instead of showing no data at all ([#62543]).
Expand Down
20 changes: 17 additions & 3 deletions base/asyncevent.jl
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ Create a timer that wakes up tasks waiting for it (by calling [`wait`](@ref) on

Waiting tasks are woken after an initial delay of at least `delay` seconds, and then repeating after
at least `interval` seconds again elapse. If `interval` is equal to `0`, the timer is only triggered
once. When the timer is closed (by [`close`](@ref)) waiting tasks are woken with an error. Use
[`isopen`](@ref) to check whether a timer is still active. An inactive timer will not fire.
once. When closing (by [`close`](@ref)) either a repeating timer or a one-shot timer before it has
triggered, waiting tasks are woken with an error. After a one-shot timer triggers, all subsequent calls
to [`wait`](@ref) return immediately, even if it is closed.
Use [`isopen`](@ref) to check whether a timer is still active. An inactive timer will not fire.
Use `t.timeout` and `t.interval` to read the setup conditions of a `Timer` `t`.

```julia-repl
Expand Down Expand Up @@ -105,6 +107,10 @@ false
!!! compat "Julia 1.12"
The `timeout` and `interval` readable properties were added in Julia 1.12.

!!! compat "Julia 1.14"
Prior to Julia 1.14, only the first call to `wait` on a triggered one-shot timer returned,
and subsequent calls threw an `EOFError`.

"""
mutable struct Timer
@atomic handle::Ptr{Cvoid}
Expand Down Expand Up @@ -168,6 +174,9 @@ function _trywait(t::Union{Timer, AsyncCondition})
t isa Timer || Core.Intrinsics.atomic_fence(:acquire_release, :system)
else
if !isopen(t)
# the :acquire read of isopen pairs with the :release store in uv_timercb, which
# sets `set` beforehand: a waiter observing the trigger-initiated close of a
# one-shot timer cannot miss the trigger on this recheck
set = t.set
if !set
close(t) # wait for the close to complete
Expand Down Expand Up @@ -195,7 +204,10 @@ function _trywait(t::Union{Timer, AsyncCondition})
end
iolock_end()
end
@atomic :monotonic t.set = false # if there are multiple waiters, an unspecified number may short-circuit past here
if !(t isa Timer && iszero(t.interval_ms))
# if there are multiple waiters, an unspecified number may short-circuit past here
@atomic :monotonic t.set = false
end
return set
end

Expand Down Expand Up @@ -295,6 +307,8 @@ function uv_timercb(handle::Ptr{Cvoid})
t = @handle_as handle Timer
lock(t.cond)
try
# this store must stay ordered before the :release store of isopen below, so that
# a waiter observing the close in _trywait is guaranteed to also observe `set`
@atomic :monotonic t.set = true
if ccall(:uv_timer_get_repeat, UInt64, (Ptr{Cvoid},), t) == 0
# timer is stopped now
Expand Down
47 changes: 47 additions & 0 deletions test/channels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,53 @@ let a = []
@test timedwait(() -> a == [1], 10) === :ok
end

@testset "wait after closing a one-shot Timer (#34366)" begin
t = Timer(600)
close(t) # test assumes that thread won't get preempted for 10 minutes...
@test_throws EOFError wait(t)

t = Timer(0)
@test wait(t) === nothing
@test wait(t) === nothing
close(t)
@test wait(t) === nothing

waiters = [Threads.@spawn wait(t) for _ in 1:8]
@test all(w -> fetch(w) === nothing, waiters)

for i in 1:100
t = Timer(isodd(i) ? 0 : 0.001)
racers = [Threads.@spawn begin
for _ in 1:rand(0:8)
yield()
end
try
wait(t)
wait(t)
true
catch e
e isa EOFError || rethrow()
false
end
end for _ in 1:4]
for _ in 1:rand(0:8)
yield()
end
close(t)
outcomes = map(fetch, racers)
@test all(outcomes) || !any(outcomes)
end

t = Timer(0, interval=600)
@test wait(t) === nothing
waiter = @task wait(t)
yield(waiter)
@test timedwait(() -> !isempty(t.cond.waitq) && first(t.cond.waitq).task === waiter, 10) === :ok
close(t)
@test_throws TaskFailedException wait(waiter)
@test waiter.result isa EOFError
end

# make sure that we don't accidentally create a one-shot timer
let
t = Timer(Returns(nothing), 10, interval=0.00001)
Expand Down
2 changes: 1 addition & 1 deletion test/threads_exec.jl
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ for period in (0.06, Dates.Millisecond(60))
close(async)
@test_throws EOFError wait(async)
@test !isopen(async)
@test_throws EOFError wait(t)
@test wait(t) === nothing
@test_throws EOFError wait(async)
end
end
Expand Down