-
Notifications
You must be signed in to change notification settings - Fork 833
Description
We observe a deadlock in FCS, likely caused by incorrect diagnostic behavior in MultipleDiagnosticsLoggers.Parallel
Consider the code:
fsharp/src/Compiler/Facilities/DiagnosticsLogger.fs
Lines 916 to 955 in cdd67c7
| 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:
- Allocates N
TaskCompletionSourceobjects - Connects every input
Asyncwith a correspondingTaskCompletionSource - Starts the input computations using
Async.Parallel - In the
finallyblock, waits forreplayDiagnostics— a task that collects every allocatedTaskCompletionSourceand 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.RunSynchronouslyYou 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
Labels
Type
Projects
Status