Skip to content

MultipleDiagnosticsLoggers.Parallel deadlock #17205

@ForNeVeR

Description

@ForNeVeR

We observe a deadlock in FCS, likely caused by incorrect diagnostic behavior in MultipleDiagnosticsLoggers.Parallel

Consider the code:

let computationsWithLoggers, diagnosticsReady =
[
for i, computation in computations |> Seq.indexed do
let diagnosticsReady = TaskCompletionSource<_>()
let logger = CapturingDiagnosticsLogger($"CaptureDiagnosticsConcurrently {i}")
// Inject capturing logger into the computation. Signal the TaskCompletionSource when done.
let computationsWithLoggers =
async {
SetThreadDiagnosticsLoggerNoUnwind logger
try
return! computation
finally
diagnosticsReady.SetResult logger
}
computationsWithLoggers, diagnosticsReady
]
|> List.unzip
// Commit diagnostics from computations as soon as it is possible, preserving the order.
let replayDiagnostics =
backgroundTask {
let target = DiagnosticsThreadStatics.DiagnosticsLogger
for tcs in diagnosticsReady do
let! finishedLogger = tcs.Task
finishedLogger.CommitDelayedDiagnostics target
}
async {
try
// We want to restore the current diagnostics context when finished.
use _ = new CompilationGlobalsScope()
return! Async.Parallel computationsWithLoggers
finally
replayDiagnostics.Wait()
}

Here, it tries to replicate the same behavior as Async.Parallel, but with additional behavior of reporting some statistics to compiler diagnostic engine.

To do that, essentially it works like this:

  1. Allocates N TaskCompletionSource objects
  2. Connects every input Async with a corresponding TaskCompletionSource
  3. Starts the input computations using Async.Parallel
  4. In the finally block, waits for replayDiagnostics — a task that collects every allocated TaskCompletionSource and reports the results

There's a problem, though: Async.Parallel doesn't give guarantees that it will start every input computation!

It has a short-circuiting property and will stop spawning new computations if it sees an error. Example code to see this behavior:

let failedTask = Task.FromException(Exception("tatata"))
let asyncs = [
    Async.AwaitTask failedTask

    for x in 1..300 do
        async {
            printfn $"{x}"
            failwith "error"
        }
]

let a = Async.Parallel asyncs
async {
    let! results = a
    return ()
} |> Async.RunSynchronously

You will see that it never executes all print 1…print 300 — because of this short-circuiting behavior of Async.Parallel.

If this property of Async.Parallel occurs in the compiler code (i.e. a computation throws an exception), then certain TaskCompletionSource objects will be orphaned (never completed, since their computations were never started to begin with), and thus MultipleDiagnosticsLoggers.Parallel call will never complete.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Done

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions