Skip to content

Commit 59b6bb1

Browse files
authored
Make ServerProgressReport threadsafe (#1130)
1 parent 4103c13 commit 59b6bb1

File tree

2 files changed

+73
-26
lines changed

2 files changed

+73
-26
lines changed

src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ open System.Collections.Concurrent
4242
open System.Diagnostics
4343
open System.Text.RegularExpressions
4444
open IcedTasks
45+
open System.Threading.Tasks
4546

4647
[<RequireQualifiedAccess>]
4748
type WorkspaceChosen =
@@ -712,8 +713,8 @@ type AdaptiveFSharpLspServer
712713

713714
use progressReport = new ServerProgressReport(lspClient)
714715

715-
progressReport.Begin($"Loading {projects.Count} Projects")
716-
|> Async.StartImmediate
716+
progressReport.Begin ($"Loading {projects.Count} Projects") (CancellationToken.None)
717+
|> ignore<Task<unit>>
717718

718719
let projectOptions =
719720
loader.LoadProjects(projects |> Seq.map (fst >> UMX.untag) |> Seq.toList, [], binlogConfig)

src/FsAutoComplete/LspServers/FSharpLspClient.fs

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ open FsAutoComplete.LspHelpers
99
open System
1010
open System.Threading.Tasks
1111
open FsAutoComplete.Utils
12+
open System.Threading
13+
open IcedTasks
1214

1315

1416
type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServerRequest: ClientRequestSender) =
@@ -89,37 +91,77 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe
8991

9092

9193

94+
/// <summary>
95+
/// An awaitable wrapper around a task whose result is disposable. The wrapper is not disposable, so this prevents usage errors like "use _lock = myAsync()" when the appropriate usage should be "use! _lock = myAsync())".
96+
/// </summary>
97+
[<Struct>]
98+
type AwaitableDisposable<'T when 'T :> IDisposable>(t: Task<'T>) =
99+
member x.GetAwaiter() = t.GetAwaiter()
100+
member x.AsTask() = t
101+
static member op_Implicit(source: AwaitableDisposable<'T>) = source.AsTask()
102+
103+
[<AutoOpen>]
104+
module private SemaphoreSlimExtensions =
105+
// Based on https://gist.github.com/StephenCleary/7dd1c0fc2a6594ba0ed7fb7ad6b590d6
106+
// and https://gist.github.com/brendankowitz/5949970076952746a083054559377e56
107+
type SemaphoreSlim with
108+
109+
member x.LockAsync(?ct: CancellationToken) =
110+
AwaitableDisposable(
111+
task {
112+
let ct = defaultArg ct CancellationToken.None
113+
let t = x.WaitAsync(ct)
114+
115+
do! t
116+
117+
return
118+
{ new IDisposable with
119+
member _.Dispose() =
120+
// only release if the task completed successfully
121+
// otherwise, we could be releasing a semaphore that was never acquired
122+
if t.Status = TaskStatus.RanToCompletion then
123+
x.Release() |> ignore }
124+
}
125+
)
126+
92127
type ServerProgressReport(lspClient: FSharpLspClient, ?token: ProgressToken) =
93128

94-
let mutable canReportProgress = true
129+
let mutable canReportProgress = false
95130
let mutable endSent = false
96131

132+
let locker = new SemaphoreSlim(1, 1)
133+
97134
member val Token = defaultArg token (ProgressToken.Second((Guid.NewGuid().ToString())))
98135

99136
member x.Begin(title, ?cancellable, ?message, ?percentage) =
100-
async {
101-
let! result = lspClient.WorkDoneProgressCreate x.Token
102-
103-
match result with
104-
| Ok() -> ()
105-
| Error e -> canReportProgress <- false
106-
107-
if canReportProgress then
108-
do!
109-
lspClient.Progress(
110-
x.Token,
111-
WorkDoneProgressBegin.Create(
112-
title,
113-
?cancellable = cancellable,
114-
?message = message,
115-
?percentage = percentage
137+
cancellableTask {
138+
use! __ = fun ct -> locker.LockAsync(ct)
139+
140+
if not endSent then
141+
let! result = lspClient.WorkDoneProgressCreate x.Token
142+
143+
match result with
144+
| Ok() -> canReportProgress <- true
145+
| Error e -> canReportProgress <- false
146+
147+
if canReportProgress then
148+
do!
149+
lspClient.Progress(
150+
x.Token,
151+
WorkDoneProgressBegin.Create(
152+
title,
153+
?cancellable = cancellable,
154+
?message = message,
155+
?percentage = percentage
156+
)
116157
)
117-
)
118158
}
119159

120160
member x.Report(?cancellable, ?message, ?percentage) =
121-
async {
122-
if canReportProgress then
161+
cancellableTask {
162+
use! __ = fun ct -> locker.LockAsync(ct)
163+
164+
if canReportProgress && not endSent then
123165
do!
124166
lspClient.Progress(
125167
x.Token,
@@ -128,14 +170,18 @@ type ServerProgressReport(lspClient: FSharpLspClient, ?token: ProgressToken) =
128170
}
129171

130172
member x.End(?message) =
131-
async {
132-
if canReportProgress && not endSent then
173+
cancellableTask {
174+
use! __ = fun ct -> locker.LockAsync(ct)
175+
let stillNeedsToSend = canReportProgress && not endSent
176+
endSent <- true
177+
178+
if stillNeedsToSend then
133179
do! lspClient.Progress(x.Token, WorkDoneProgressEnd.Create(?message = message))
134-
endSent <- true
135180
}
136181

137182
interface IAsyncDisposable with
138-
member x.DisposeAsync() = task { do! x.End() } |> ValueTask
183+
member x.DisposeAsync() =
184+
task { do! x.End () (CancellationToken.None) } |> ValueTask
139185

140186
interface IDisposable with
141187
member x.Dispose() =

0 commit comments

Comments
 (0)