Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Uncaught exceptions from tasks #32034

Closed
jonas-schulze opened this issue May 15, 2019 · 8 comments
Closed

Uncaught exceptions from tasks #32034

jonas-schulze opened this issue May 15, 2019 · 8 comments

Comments

@jonas-schulze
Copy link
Contributor

jonas-schulze commented May 15, 2019

If a task throws an exception it goes down silently. This issue was reported before (#12485) and seemed to be fixed in version 1.1 (#28878), but it popped up (or it didn't) in one of my unit tests. A similar scenario:

broken example

c = Channel{Int}(2)

@async begin
  for i in 1:2
    for j in 1:2
      put!(c, i+j)
    end
    close(c)
  end
end

@sync for _ in 1:Sys.CPU_THREADS
  @async for x in c 
    println(x)
  end
end

Output:

2
3

corrected example

c = Channel{Int}(2)

@async begin
  for i in 1:2
    for j in 1:2
      put!(c, i+j)
    end
  end
  close(c) # only line that changed
end

@sync for _ in 1:Sys.CPU_THREADS
  @async for x in c 
    println(x)
  end
end

Output:

2
3
3
4

versioninfo

Julia Version 1.1.0
Commit 80516ca202 (2019-01-21 21:24 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin14.5.0)
  CPU: Intel(R) Core(TM) i5-8259U CPU @ 2.30GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.1 (ORCJIT, skylake)
Environment:
  JULIA_NUM_THREADS = 4
@JeffBezanson
Copy link
Sponsor Member

I don't think this is exactly the same as those issues, but it is more like #7626 and #10405. See discussion there.

@jonas-schulze
Copy link
Contributor Author

Thanks for the pointers! Is there a way to have those exceptions (within tasks that were spawned by the current package's code) be caught automatically in package tests?

If not, I think any unobserved task should be considered a code smell. Every @async ... should be replaced with t = @async ... and a Golang-like defer wait(t) or something to catch the error.

@JeffBezanson
Copy link
Sponsor Member

It's very hard to have an automatic fallback for otherwise-uncaught exceptions in tasks, since in general it requires looking into the future: if somebody is going to call wait(t) and handle the exception then fine, otherwise use the fallback handler. We don't know whether a wait call will happen.

@JeffBezanson
Copy link
Sponsor Member

We could add something like @monitored_async that prints exceptions from the task though.

@jonas-schulze
Copy link
Contributor Author

That sounds like a good idea. Having a way to make that the default (for ]test if one so desires) would be even better! Maybe @async and family could become generated functions (generated macros?) that check an environment variable, which would only be set for ]test (or by default in Julia 2). Alternatively, there could be something like a "compute context":

The parser should know whether a just created task object is stored somewhere (e.g. t = @async ...) or whether the return value is being ignored (e.g. plain @async ...). Maybe it can add all or all those uncaught tasks to a storage inside the current compute context; similar to how @sync keeps track of most of the enclosed tasks, but recursive. If there is no context, no task is being stored (just like now). This would allow for even finer control:

ctx = ComputeContext()
@monitor ctx begin
    # spawn some tasks

    # unrelated/detail: maybe the task constructor should know of the `ctx`
    # instead of `@async`: Task(...; ctx=currentcomputecontext())
end

foreach(wait, tasks(ctx)) # or a new `wait(ctx)`

Running runtests.jl within ]test could (should?) be wrapped implicitly inside such a context. One would need to find a solution for constants defined within that block though.

Such a compute context could also be used for handling cancellation of tasks. Check out golang/go#29011 and the references therein (the above ComputeContext sounds a lot like a "nursery" described in that link). Julia's @async is basically the same as Golang's go.

@JeffBezanson
Copy link
Sponsor Member

Some kind of task context construct is a good idea; that could be discussed in a separate proposal.

It's hard to do any sort of global default behavior switch here, because of libraries. You never know what code might be spawning tasks. I would also be against changing this default in Julia 2.0. The problem is not that it's a breaking change, but that it's impossible to know whether an exception in a task will be handled eventually.

Maybe @async and family could become generated functions (generated macros?) that check an environment variable

Generated functions should absolutely not check environment variables; they need to be pure.

The parser should know whether a just created task object is stored somewhere

This is a good thought, but this kind of static analysis is very coarse. For example, you might write t = @async ... but then not use t. It would be surprising if adding an unused variable assignment changed exception throwing behavior.

@jonas-schulze
Copy link
Contributor Author

jonas-schulze commented May 15, 2019

it's impossible to know whether an exception in a task will be handled eventually.

Of those whose return value of the creating code (constructor, @async, ...) isn't caught and that are not enclosed by a @sync block, we do know. That would be a good starting point I think.

Generated functions should absolutely not check environment variables; they need to be pure.

Once the generation is done, they will be pure. What I have in mind is something like

@generated foo() = get(ENV, "FOO", "") != "" ? :(42) : :(21)

but for macros. Maybe like so (I'm not that proficient writing macros):

@generated macro async(f)
    if parse(Bool, get(ENV, "JULIA_MONITOR_TASKS", "false"))
        :(@monitored_async f)
    else
        :(@unmonitored_async f)
    end
end

@JeffBezanson
Copy link
Sponsor Member

Once the generation is done, they will be pure. What I have in mind is something like

The code generated by a @generated function does not have to be pure. It's the generator that does.

Macros don't need a @generated equivalent, since they already run at an earlier stage before execution. Your macro works if you just remove @generated. However, it will only check the variable at parse time, and so that won't include any pre-compiled code. For this to be useful I think we'd have to check a flag at run time. It only affects exception paths so overhead is probably not a concern.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants