Fix: Occasionally publishDiagnostics
is not sent
#887
Merged
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
tl;dr: There's a lock issue in
DiagnosticCollection
which results in an ignored exception (-> eventtextDocument/publishDiagnostics
doesn't get sent)Description
(slightly verbose because I used this (at least to some extend) to structure my findings and detect where to look next)
LSP-Event
textDocument/publishDiagnostics
is used to send Diagnostics. But sometimes it doesn't get sent when it should be.In FSAC
publishDiagnostics
gets send after: F# parsing, after each built-in Analyzer, after Custom Analyzers and (currently uncommented) after Linting.When all three built-in analyzers (UnusedOpensAnalyzer, UnusedDeclarationsAnalyzer, SimplifyNameAnalyzer) are enabled,
publishDiagnostics
should occur four times: once for F# Compiler, and once for each Analyzer. (Sending happens inFsAutoComplete.Lsp.fs::DiagnosticCollection
)But occasionally an event doesn't get sent.
It's not deterministic -> no simple test to reproduce error.
But by running a test quite often it's likely the failure emerges (at least on my PC):
dotnet run --project ./test/FsAutoComplete.Tests.Lsp -- --filter "lsp.Ionide WorkspaceLoader.diagnostics tests" --exclude-from-log=Checker,CheckerEvents,Commands,Opts,Analyzers,LSP
The tests are all the same -- it just gets execute quite often (100 times).
The test itself is quite simple: (shortened)
-> It just creates a new text document and waits for all
4
publishDiagnostics
events. Fails when it doesn't get all4
events in3s
.Note: I'm deliberately using
dotnet run
with quite some logging instead ofdotnet test
: Makes the Issue far more likely to emerge. In fact: withdotnet test
, the new tests never failed on my PC. (Logging is quite important as it makes the issue more likely to appear.)Reason
Diagnostics are collected and sent in
FsAutoComplete.Lsp.fs::DiagnosticsCollection
: It collects Diags for each file and each diag source (F# compiler, built-in analyzer, ...):SetFor(fileUri, kind, values: Diagnostic[])
:gets a mailbox for passed
fileUri
(->getOrAddAgent (fileUri)
),and then sends the diags and kind to the mailbox loop
kind
is diag source likeF# compiler
orF# Unused opens
agentFor(uri)
:handles changed diagnostics for an uri: remembers diags for each kind and sends out all diags for that uri on every received mailbox message (-> every parse step).
Logging shows: Getting a mailbox in
getOrAddAgent
blocks when two want a mailbox for the same uri at the same time, but the mailbox doesn't exist yet.In Log Messages:
For every kind (
F# Compiler
,F# Unused delcarations
,F# simplify names
,F# unused opens
) there should be:But:
F# Unused declarations
does never return fromgetOrAdd
with a mailbox (no<< mailbox(F# Unused declarations, ...)
)getOrAddAgent
:-> Both
Compiler
andUnused Declarations
want to create a mailbox because it doesn't yet exist. But onlyCompiler
actually exits with a mailbox.The other one fails because
agents
already contains the keyfileUri
->ArgumentException
with MessageAn item with the same key has already been added.
(-> inserts a new mailbox for an already existing uri)Further Complication: The exception doesn't surface, but instead gets swallowed.
In practice probably not really an issue:
publishDiagnostics
for that file, future parses/publishDiags
for that file aren't affected any more.Fix
Use
ConcurrentDictionary
instead ofDictionary
and custom lockingNote:
I didn't add any tests (or removed the example tests again):
Solution is something external (->
ConcurrentDictionary
).Additional: issue was not deterministic but just: run this over and over again and MAYBE it will fail...
Additional:
handleCommandEvents
Additional
I added two command line log filters for tests (via
dotnet run --project ./test/FsAutoComplete.Tests.Lsp -- ...
):--log={level}
: Sets Log Level.{level}
is something likewarn
orinfo
.--debug
(= verbose logging) takes precedence over--log={level}
--log=
: first occurrence is used--exclude-from-log={logger names}
: Filters out loggers with the specified namessrc
s; intest
s Expecto is used for logging){log sources}
: Comma-separated list with logger names. Like--exclude-from-log=Checker,Commands,Opts,LSP
(-> doesn't support spaces)
--exclude-from-log=Checker,Commands,Opts,LSP
=--exclude-from-log=Checker,Commands --exclude-from-log=Opts,LSP
)I implemented these to help me isolate the issue above (by reducing log clutter).
Might be useful for someone else too
-> I left these command line options in this PR.
Notes:
xxx=y[,y,y]
because it's easy to parse (vs. separated by spaces)