JuliaLang / julia

The Julia Programming Language
https://julialang.org/
MIT License
45.69k stars 5.48k forks source link

Channel task failures are no longer propagated #51597

Open jonas-schulze opened 1 year ago

jonas-schulze commented 1 year ago

If the function passed to the channel ctor fails after having produced some elements, the exception raised is being lost. This is a regression starting in Julia 1.8. I tested Julia versions 1.6.7, 1.7.3, 1.8.5, and 1.9.3.

julia> versioninfo()
Julia Version 1.8.5
Commit 17cfb8e65ea (2023-01-08 06:45 UTC)
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 16 × AMD Ryzen 7 PRO 5850U with Radeon Graphics
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-13.0.1 (ORCJIT, znver3)
  Threads: 1 on 16 virtual cores
Environment:
  JULIA_PROJECT = @.
# mwe.jl
c = Channel() do ch
    put!(ch, 42)
    error("foo")
end
foreach(println, c)

Expected behavior:

$ julia-1.6 mwe.jl 
42
ERROR: LoadError: TaskFailedException
Stacktrace:
 [1] check_channel_state
   @ ./channels.jl:170 [inlined]
 [2] take_unbuffered(c::Channel{Any})
   @ Base ./channels.jl:403
 [3] take!
   @ ./channels.jl:383 [inlined]
 [4] iterate(c::Channel{Any}, state::Nothing)
   @ Base ./channels.jl:465
 [5] foreach(f::typeof(println), itr::Channel{Any})
   @ Base ./abstractarray.jl:2141
 [6] top-level scope
   @ ~/tmp/julia-channel-task/mwe.jl:6

    nested task error: foo
    Stacktrace:
     [1] error(s::String)
       @ Base ./error.jl:33
     [2] (::var"#1#2")(ch::Channel{Any})
       @ Main ~/tmp/julia-channel-task/mwe.jl:4
     [3] (::Base.var"#517#518"{var"#1#2", Channel{Any}})()
       @ Base ./channels.jl:132
in expression starting at /home/jschulze/tmp/julia-channel-task/mwe.jl:6
jonas-schulze commented 1 year ago

This is similar to #35177 and #35673, but the example task shown there fails before having produced any elements. This failure is detected or propagated as expected.

Furthermore, it is a bit tricky to write a test for this, as the behavior seems to change when using for _ in c end over foreach(_ -> (), c) and whether this is nested in a @test_throws or @testset.

# mwe2.jl
using Test

c0() = Channel(_ -> error("foo"))
c1() = Channel() do ch
    put!(ch, 42)
    error("foo")
end
noop(_) = nothing

@testset "Channel Task Failures" begin
    @test_throws TaskFailedException for _ in c0() end
    @test_throws TaskFailedException for _ in c1() end
    @test_throws TaskFailedException foreach(println, c0())
    @test_throws TaskFailedException foreach(println, c1()) # only one that breaks
    @test_throws TaskFailedException foreach(noop, c1())
end
yha commented 1 year ago

There seems to be a race condition, at least in the case of failure before the first element. This swallows the error:

c = Channel() do ch
    error("foo")
end
sleep(0.001)
iterate(c) # returns `nothing`

and when removing the sleep, the error propagates nested in a TaskFailedException (as in Julia 1.7).

There is also a regression in the case of close(channel, exception). This swallows the error on 1.8 and 1.9:

s = Channel() do ch
    close(ch, ErrorException("foo"))
end
iterate(s)
julia> versioninfo()
Julia Version 1.9.0
Commit 8e630552924 (2023-05-07 11:25 UTC)
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 8 × AMD Ryzen 3 3100 4-Core Processor
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-14.0.6 (ORCJIT, znver2)
  Threads: 1 on 8 virtual cores
Environment:
  JULIA_EDITOR = code
  JULIA_NUM_THREADS =