Channel doesn't close when bound task exits

We have some code that was working fine until we migrated to julia-1.10 and now with Julia 1.10.2 it’s quite flaky. I’ve reduced the code to the example below, which I think shouldn’t violate the assertion, but it does.

The absurd thing is it still kinda works in VScode REPL, but it’s flaky. When I run it in terminal REPL it breaks nearly 100% of the time.

Am I wrong in my expectation this should work? Is this a bug?

function repro()
    taskref = Ref{Task}()
    feedback = Channel(_ -> nothing; taskref)
    wait(taskref[])

    # isopen(feedback) && sleep(0.1)
    @assert !isopen(feedback) # this fails unless the line above is uncommented
end

repro()

I’m trying this on Linux with julia 1.9.3 and 1.10.2

I think this might be a bug. I was going to recommend that you instead wait on the Channel, but then I ran into this surprise:

julia> function repro2(cond)
           taskref = Ref{Task}()
           feedback = Channel(_ -> nothing; taskref)
           if cond
               wait(feedback)
           end
           isopen(feedback)
       end;

julia> repro2(false)
true

julia> repro2(true)
ERROR: InvalidStateException: Channel is closed.
Stacktrace:
 [1] try_yieldto(undo::typeof(Base.ensure_rescheduled))
   @ Base ./task.jl:931
 [2] wait()
   @ Base ./task.jl:995
 [3] wait(c::Base.GenericCondition{ReentrantLock}; first::Bool)
   @ Base ./condition.jl:130
 [4] wait
   @ ./condition.jl:125 [inlined]
 [5] wait(c::Channel{Any})
   @ Base ./channels.jl:581
 [6] repro2(cond::Bool)
   @ Main ./REPL[39]:5
 [7] top-level scope

so waiting on the Channel errors claiming the channel is closed, but calling isopen on the channel reports that it’s open!

Yeah, with your demonstration it’s quite clear it’s a bug.

Also, to clarify, this is not a regression from 1.9 to 1.10, the reduced example breaks in both. For some reason, our original code works reliably in 1.9, and flaky in 1.10.

Would you mind opening an issue on Github?

I came to conclusion that your suggestion to use wait(feedback) is the right way to handle the situation. It’s mildly annoying that wait(::Channel) throws, but that’s a separate discussion.

In principle you shouldn’t rely on isopen() telling you the “true” state, as in multitasking system, the state can change an instant later. So, you should be using fetch()/take!()/wait(::Channel) are more reliable.

TLDR, it’s not a bug, it’s user error.

1 Like

It looks like there’s no special magic behind bind(ch::Channel, t::Task). Effectively it does @async begin ; wait(t) ; close(ch); end + some exception handling. So @assert !isopen(feedback) may or may not fail depending on how exactly tasks get scheduled. This probably explains why our original code got flaky on 1.10, it wasn’t robust to start with.

1 Like