diff --git a/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md index 6544ab4685f..1cd2b82d889 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/8.0.300.md @@ -13,4 +13,5 @@ * `implicitCtorSynPats` in `SynTypeDefnSimpleRepr.General` is now `SynPat option` instead of `SynSimplePats option`. ([PR #16425](https://github.com/dotnet/fsharp/pull/16425)) * `SyntaxVisitorBase<'T>.VisitSimplePats` now takes `SynPat` instead of `SynSimplePat list`. ([PR #16425](https://github.com/dotnet/fsharp/pull/16425)) -* Reduce allocations in compiler checking via `ValueOption` usage ([PR #16323](https://github.com/dotnet/fsharp/pull/16323)) \ No newline at end of file +* Reduce allocations in compiler checking via `ValueOption` usage ([PR #16323](https://github.com/dotnet/fsharp/pull/16323)) +* Reverted [#16348](https://github.com/dotnet/fsharp/pull/16348) `ThreadStatic` `CancellationToken` changes to improve test stability and prevent potential unwanted cancellations. ([PR #16536](https://github.com/dotnet/fsharp/pull/16536)) \ No newline at end of file diff --git a/src/Compiler/Facilities/AsyncMemoize.fs b/src/Compiler/Facilities/AsyncMemoize.fs index ad798140cb9..8309eaa1c15 100644 --- a/src/Compiler/Facilities/AsyncMemoize.fs +++ b/src/Compiler/Facilities/AsyncMemoize.fs @@ -346,7 +346,6 @@ type internal AsyncMemoize<'TKey, 'TVersion, 'TValue when 'TKey: equality and 'T try // TODO: Should unify starting and restarting - use _ = Cancellable.UsingToken(cts.Token) log (Started, key) Interlocked.Increment &restarted |> ignore System.Diagnostics.Trace.TraceInformation $"{name} Restarted {key.Label}" @@ -498,7 +497,7 @@ type internal AsyncMemoize<'TKey, 'TVersion, 'TValue when 'TKey: equality and 'T // TODO: Should unify starting and restarting let currentLogger = DiagnosticsThreadStatics.DiagnosticsLogger DiagnosticsThreadStatics.DiagnosticsLogger <- cachingLogger - use _ = Cancellable.UsingToken(internalCt) + log (Started, key) try diff --git a/src/Compiler/Facilities/BuildGraph.fs b/src/Compiler/Facilities/BuildGraph.fs index 8927862c23c..1df58c1024b 100644 --- a/src/Compiler/Facilities/BuildGraph.fs +++ b/src/Compiler/Facilities/BuildGraph.fs @@ -17,14 +17,12 @@ let wrapThreadStaticInfo computation = async { let diagnosticsLogger = DiagnosticsThreadStatics.DiagnosticsLogger let phase = DiagnosticsThreadStatics.BuildPhase - let ct = Cancellable.Token try return! computation finally DiagnosticsThreadStatics.DiagnosticsLogger <- diagnosticsLogger DiagnosticsThreadStatics.BuildPhase <- phase - Cancellable.Token <- ct } type Async<'T> with @@ -127,7 +125,6 @@ type NodeCode private () = static member RunImmediate(computation: NodeCode<'T>, ct: CancellationToken) = let diagnosticsLogger = DiagnosticsThreadStatics.DiagnosticsLogger let phase = DiagnosticsThreadStatics.BuildPhase - let ct2 = Cancellable.Token try try @@ -135,7 +132,6 @@ type NodeCode private () = async { DiagnosticsThreadStatics.DiagnosticsLogger <- diagnosticsLogger DiagnosticsThreadStatics.BuildPhase <- phase - Cancellable.Token <- ct2 return! computation |> Async.AwaitNodeCode } @@ -143,7 +139,6 @@ type NodeCode private () = finally DiagnosticsThreadStatics.DiagnosticsLogger <- diagnosticsLogger DiagnosticsThreadStatics.BuildPhase <- phase - Cancellable.Token <- ct2 with :? AggregateException as ex when ex.InnerExceptions.Count = 1 -> raise (ex.InnerExceptions[0]) @@ -153,14 +148,12 @@ type NodeCode private () = static member StartAsTask_ForTesting(computation: NodeCode<'T>, ?ct: CancellationToken) = let diagnosticsLogger = DiagnosticsThreadStatics.DiagnosticsLogger let phase = DiagnosticsThreadStatics.BuildPhase - let ct2 = Cancellable.Token try let work = async { DiagnosticsThreadStatics.DiagnosticsLogger <- diagnosticsLogger DiagnosticsThreadStatics.BuildPhase <- phase - Cancellable.Token <- ct2 return! computation |> Async.AwaitNodeCode } @@ -168,7 +161,6 @@ type NodeCode private () = finally DiagnosticsThreadStatics.DiagnosticsLogger <- diagnosticsLogger DiagnosticsThreadStatics.BuildPhase <- phase - Cancellable.Token <- ct2 static member CancellationToken = cancellationToken diff --git a/src/Compiler/Interactive/fsi.fs b/src/Compiler/Interactive/fsi.fs index ca0e2335064..e5ff5b6c754 100644 --- a/src/Compiler/Interactive/fsi.fs +++ b/src/Compiler/Interactive/fsi.fs @@ -4089,7 +4089,6 @@ type FsiInteractionProcessor ?cancellationToken: CancellationToken ) = let cancellationToken = defaultArg cancellationToken CancellationToken.None - use _ = Cancellable.UsingToken(cancellationToken) if tokenizer.LexBuffer.IsPastEndOfStream then let stepStatus = @@ -4218,7 +4217,6 @@ type FsiInteractionProcessor member _.EvalInteraction(ctok, sourceText, scriptFileName, diagnosticsLogger, ?cancellationToken) = let cancellationToken = defaultArg cancellationToken CancellationToken.None - use _ = Cancellable.UsingToken(cancellationToken) use _ = UseBuildPhase BuildPhase.Interactive use _ = UseDiagnosticsLogger diagnosticsLogger use _scope = SetCurrentUICultureForThread fsiOptions.FsiLCID @@ -4895,7 +4893,6 @@ type FsiEvaluationSession SpawnInteractiveServer(fsi, fsiOptions, fsiConsoleOutput) use _ = UseBuildPhase BuildPhase.Interactive - use _ = Cancellable.UsingToken(CancellationToken.None) if fsiOptions.Interact then // page in the type check env diff --git a/src/Compiler/Service/BackgroundCompiler.fs b/src/Compiler/Service/BackgroundCompiler.fs index 514bb8e45c5..f9f952dde70 100644 --- a/src/Compiler/Service/BackgroundCompiler.fs +++ b/src/Compiler/Service/BackgroundCompiler.fs @@ -574,9 +574,6 @@ type internal BackgroundCompiler Activity.Tags.cache, cache.ToString() |] - let! ct = Async.CancellationToken - use _ = Cancellable.UsingToken(ct) - if cache then let hash = sourceText.GetHashCode() |> int64 @@ -629,9 +626,6 @@ type internal BackgroundCompiler "BackgroundCompiler.GetBackgroundParseResultsForFileInProject" [| Activity.Tags.fileName, fileName; Activity.Tags.userOpName, userOpName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! builderOpt, creationDiags = getOrCreateBuilder (options, userOpName) match builderOpt with @@ -783,9 +777,6 @@ type internal BackgroundCompiler Activity.Tags.userOpName, userOpName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! cachedResults = node { let! builderOpt, creationDiags = getAnyBuilder (options, userOpName) @@ -846,9 +837,6 @@ type internal BackgroundCompiler Activity.Tags.userOpName, userOpName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! builderOpt, creationDiags = getOrCreateBuilder (options, userOpName) match builderOpt with @@ -897,9 +885,6 @@ type internal BackgroundCompiler Activity.Tags.userOpName, userOpName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! builderOpt, creationDiags = getOrCreateBuilder (options, userOpName) match builderOpt with @@ -969,9 +954,6 @@ type internal BackgroundCompiler Activity.Tags.userOpName, userOpName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! builderOpt, _ = getOrCreateBuilder (options, userOpName) match builderOpt with @@ -991,9 +973,6 @@ type internal BackgroundCompiler Activity.Tags.userOpName, userOpName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! builderOpt, creationDiags = getOrCreateBuilder (options, userOpName) match builderOpt with @@ -1134,9 +1113,6 @@ type internal BackgroundCompiler Activity.Tags.userOpName, userOpName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! builderOpt, _ = getOrCreateBuilder (options, userOpName) match builderOpt with @@ -1185,8 +1161,6 @@ type internal BackgroundCompiler /// Parse and typecheck the whole project (the implementation, called recursively as project graph is evaluated) member private _.ParseAndCheckProjectImpl(options, userOpName) = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) let! builderOpt, creationDiags = getOrCreateBuilder (options, userOpName) @@ -1452,8 +1426,6 @@ type internal BackgroundCompiler |] async { - let! ct = Async.CancellationToken - use _ = Cancellable.UsingToken(ct) let! ct = Async.CancellationToken // If there was a similar entry (as there normally will have been) then re-establish an empty builder . This diff --git a/src/Compiler/Service/FSharpCheckerResults.fs b/src/Compiler/Service/FSharpCheckerResults.fs index 48b9875c9f0..5f18a90968a 100644 --- a/src/Compiler/Service/FSharpCheckerResults.fs +++ b/src/Compiler/Service/FSharpCheckerResults.fs @@ -3585,9 +3585,6 @@ type FsiInteractiveChecker(legacyReferenceResolver, tcConfig: TcConfig, tcGlobal member _.ParseAndCheckInteraction(sourceText: ISourceText, ?userOpName: string) = cancellable { - let! ct = Cancellable.token () - use _ = Cancellable.UsingToken(ct) - let userOpName = defaultArg userOpName "Unknown" let fileName = Path.Combine(tcConfig.implicitIncludeDir, "stdin.fsx") let suggestNamesForErrors = true // Will always be true, this is just for readability diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs index fe82627483f..298c0e6b627 100644 --- a/src/Compiler/Service/TransparentCompiler.fs +++ b/src/Compiler/Service/TransparentCompiler.fs @@ -1762,8 +1762,6 @@ type internal TransparentCompiler node { //use _ = // Activity.start "ParseFile" [| Activity.Tags.fileName, fileName |> Path.GetFileName |] - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) // TODO: might need to deal with exceptions here: let tcConfigB, sourceFileNames, _ = ComputeTcConfigBuilder projectSnapshot @@ -1818,9 +1816,6 @@ type internal TransparentCompiler userOpName: string ) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! snapshot = FSharpProjectSnapshot.FromOptions(options, fileName, fileVersion, sourceText) |> NodeCode.AwaitAsync @@ -1842,9 +1837,6 @@ type internal TransparentCompiler userOpName: string ) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! snapshot = FSharpProjectSnapshot.FromOptions(options, fileName, fileVersion, sourceText) |> NodeCode.AwaitAsync @@ -1897,9 +1889,6 @@ type internal TransparentCompiler userOpName: string ) : NodeCode> = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - ignore canInvalidateProject let! snapshot = FSharpProjectSnapshot.FromOptions options |> NodeCode.AwaitAsync @@ -1914,9 +1903,6 @@ type internal TransparentCompiler member this.GetAssemblyData(options: FSharpProjectOptions, fileName, userOpName: string) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! snapshot = FSharpProjectSnapshot.FromOptions options |> NodeCode.AwaitAsync return! this.GetAssemblyData(snapshot.ProjectSnapshot, fileName, userOpName) } @@ -1936,9 +1922,6 @@ type internal TransparentCompiler userOpName: string ) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! snapshot = FSharpProjectSnapshot.FromOptions options |> NodeCode.AwaitAsync match! this.ParseAndCheckFileInProject(fileName, snapshot.ProjectSnapshot, userOpName) with @@ -1953,9 +1936,6 @@ type internal TransparentCompiler userOpName: string ) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! snapshot = FSharpProjectSnapshot.FromOptions options |> NodeCode.AwaitAsync return! this.ParseFile(fileName, snapshot.ProjectSnapshot, userOpName) } @@ -1969,8 +1949,6 @@ type internal TransparentCompiler ) : NodeCode<(FSharpParseFileResults * FSharpCheckFileResults) option> = node { ignore builder - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) let! snapshot = FSharpProjectSnapshot.FromOptions(options, fileName, 1, sourceText) @@ -2023,9 +2001,6 @@ type internal TransparentCompiler ) : NodeCode = node { ignore userOpName - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! snapshot = FSharpProjectSnapshot.FromOptions options |> NodeCode.AwaitAsync return! ComputeSemanticClassification(fileName, snapshot.ProjectSnapshot) } @@ -2048,9 +2023,6 @@ type internal TransparentCompiler userOpName: string ) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - let! snapshot = FSharpProjectSnapshot.FromOptions(options, fileName, fileVersion, sourceText) |> NodeCode.AwaitAsync @@ -2063,9 +2035,6 @@ type internal TransparentCompiler member this.ParseAndCheckProject(options: FSharpProjectOptions, userOpName: string) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - ignore userOpName let! snapshot = FSharpProjectSnapshot.FromOptions options |> NodeCode.AwaitAsync return! ComputeParseAndCheckProject snapshot.ProjectSnapshot @@ -2073,9 +2042,6 @@ type internal TransparentCompiler member this.ParseAndCheckProject(projectSnapshot: FSharpProjectSnapshot, userOpName: string) : NodeCode = node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - ignore userOpName return! ComputeParseAndCheckProject projectSnapshot.ProjectSnapshot } diff --git a/src/Compiler/Service/service.fs b/src/Compiler/Service/service.fs index b79254d7935..492ff2da497 100644 --- a/src/Compiler/Service/service.fs +++ b/src/Compiler/Service/service.fs @@ -348,9 +348,6 @@ type FSharpChecker use _ = Activity.start "FSharpChecker.Compile" [| Activity.Tags.userOpName, _userOpName |] async { - let! ct = Async.CancellationToken - use _ = Cancellable.UsingToken(ct) - let ctok = CompilationThreadToken() return CompileHelpers.compileFromArgs (ctok, argv, legacyReferenceResolver, None, None) } @@ -485,9 +482,6 @@ type FSharpChecker let userOpName = defaultArg userOpName "Unknown" node { - let! ct = NodeCode.CancellationToken - use _ = Cancellable.UsingToken(ct) - if fastCheck <> Some true || not captureIdentifiersWhenParsing then return! backgroundCompiler.FindReferencesInFile(fileName, options, symbol, canInvalidateProject, userOpName) else diff --git a/src/Compiler/Utilities/Cancellable.fs b/src/Compiler/Utilities/Cancellable.fs index c702e3b7a0b..59e7def4c10 100644 --- a/src/Compiler/Utilities/Cancellable.fs +++ b/src/Compiler/Utilities/Cancellable.fs @@ -2,32 +2,47 @@ namespace FSharp.Compiler open System open System.Threading +open Internal.Utilities.Library [] type Cancellable = [] - static val mutable private token: CancellationToken - - static member UsingToken(ct) = - let oldCt = Cancellable.token - - Cancellable.token <- ct + static val mutable private tokens: CancellationToken list + static let disposable = { new IDisposable with - member this.Dispose() = Cancellable.token <- oldCt + member this.Dispose() = + Cancellable.Tokens <- Cancellable.Tokens |> List.tail } - static member Token - with get () = Cancellable.token - and internal set v = Cancellable.token <- v + static member Tokens + with private get () = + match box Cancellable.tokens with + | Null -> [] + | _ -> Cancellable.tokens + and private set v = Cancellable.tokens <- v + + static member UsingToken(ct) = + Cancellable.Tokens <- ct :: Cancellable.Tokens + disposable + + static member Token = + match Cancellable.Tokens with + | [] -> CancellationToken.None + | token :: _ -> token + /// There may be multiple tokens if `UsingToken` is called multiple times, producing scoped structure. + /// We're interested in the current, i.e. the most recent, one. static member CheckAndThrow() = - Cancellable.token.ThrowIfCancellationRequested() + match Cancellable.Tokens with + | [] -> () + | token :: _ -> token.ThrowIfCancellationRequested() namespace Internal.Utilities.Library open System open System.Threading +open FSharp.Compiler #if !FSHARPCORE_USE_PACKAGE open FSharp.Core.CompilerServices.StateMachineHelpers @@ -48,6 +63,7 @@ module Cancellable = ValueOrCancelled.Cancelled(OperationCanceledException ct) else try + use _ = Cancellable.UsingToken(ct) oper ct with :? OperationCanceledException as e -> ValueOrCancelled.Cancelled(OperationCanceledException e.CancellationToken) diff --git a/src/Compiler/Utilities/Cancellable.fsi b/src/Compiler/Utilities/Cancellable.fsi index 6e36d7ecb6d..23515432bdd 100644 --- a/src/Compiler/Utilities/Cancellable.fsi +++ b/src/Compiler/Utilities/Cancellable.fsi @@ -7,7 +7,6 @@ open System.Threading type Cancellable = static member internal UsingToken: CancellationToken -> IDisposable static member Token: CancellationToken - static member internal Token: CancellationToken with set static member CheckAndThrow: unit -> unit namespace Internal.Utilities.Library diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerService/AsyncMemoize.fs b/tests/FSharp.Compiler.ComponentTests/CompilerService/AsyncMemoize.fs index 817a3b8c70a..71dbb5c957f 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerService/AsyncMemoize.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerService/AsyncMemoize.fs @@ -11,28 +11,23 @@ open FSharp.Compiler.DiagnosticsLogger open FSharp.Compiler.Diagnostics open FSharp.Compiler.BuildGraph -[] -let ``Stack trace`` () = - let memoize = AsyncMemoize() +let timeout = TimeSpan.FromSeconds 10 - let computation key = node { - // do! Async.Sleep 1 |> NodeCode.AwaitAsync +let waitFor (mre: ManualResetEvent) = + if not <| mre.WaitOne timeout then + failwith "waitFor timed out" - let! result = memoize.Get'(key * 2, node { - //do! Async.Sleep 1 |> NodeCode.AwaitAsync - return key * 5 - }) - return result * 2 +let rec internal spinFor (duration: TimeSpan) = + node { + let sw = Stopwatch.StartNew() + do! Async.Sleep 10 |> NodeCode.AwaitAsync + let remaining = duration - sw.Elapsed + if remaining > TimeSpan.Zero then + return! spinFor remaining } - //let _r2 = computation 10 - - let result = memoize.Get'(1, computation 1) |> NodeCode.RunImmediateWithoutCancellation - - Assert.Equal(10, result) - [] let ``Basics``() = @@ -74,16 +69,21 @@ let ``We can cancel a job`` () = let jobStarted = new ManualResetEvent(false) - let computation key = node { - jobStarted.Set() |> ignore - do! Async.Sleep 1000 |> NodeCode.AwaitAsync + let jobCanceled = new ManualResetEvent(false) + + let computation action = node { + action() |> ignore + do! spinFor timeout failwith "Should be canceled before it gets here" - return key * 2 } - let eventLog = ResizeArray() - let memoize = AsyncMemoize() - memoize.OnEvent(fun (e, (_label, k, _version)) -> eventLog.Add (e, k)) + let eventLog = ConcurrentQueue() + let memoize = AsyncMemoize() + memoize.OnEvent(fun (e, (_label, k, _version)) -> + eventLog.Enqueue (e, k) + if e = Canceled then + jobCanceled.Set() |> ignore + ) use cts1 = new CancellationTokenSource() use cts2 = new CancellationTokenSource() @@ -91,26 +91,22 @@ let ``We can cancel a job`` () = let key = 1 - let _task1 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts1.Token) + let _task1 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation jobStarted.Set), ct = cts1.Token) - jobStarted.WaitOne() |> ignore + waitFor jobStarted jobStarted.Reset() |> ignore - let _task2 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts2.Token) - let _task3 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts3.Token) - - Assert.Equal<(JobEvent * int) array>([| Started, key |], eventLog |> Seq.toArray ) - - do! Task.Delay 100 + let _task2 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation ignore), ct = cts2.Token) + let _task3 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation ignore), ct = cts3.Token) cts1.Cancel() cts2.Cancel() - do! Task.Delay 100 + waitFor jobStarted cts3.Cancel() - do! Task.Delay 100 + waitFor jobCanceled Assert.Equal<(JobEvent * int) array>([| Started, key; Started, key; Canceled, key |], eventLog |> Seq.toArray ) } @@ -120,66 +116,17 @@ let ``Job is restarted if first requestor cancels`` () = task { let jobStarted = new ManualResetEvent(false) - let computation key = node { - jobStarted.Set() |> ignore - - for _ in 1 .. 5 do - do! Async.Sleep 100 |> NodeCode.AwaitAsync - - return key * 2 - } - - let eventLog = ConcurrentBag() - let memoize = AsyncMemoize() - memoize.OnEvent(fun (e, (_, k, _version)) -> eventLog.Add (DateTime.Now.Ticks, (e, k))) - - use cts1 = new CancellationTokenSource() - use cts2 = new CancellationTokenSource() - use cts3 = new CancellationTokenSource() - - let key = 1 - - let _task1 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts1.Token) - jobStarted.WaitOne() |> ignore - - let _task2 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts2.Token) - let _task3 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts3.Token) - - do! Task.Delay 100 - - cts1.Cancel() - - do! Task.Delay 100 - cts3.Cancel() - - let! result = _task2 - Assert.Equal(2, result) - - Assert.Equal(TaskStatus.Canceled, _task1.Status) - - let orderedLog = eventLog |> Seq.sortBy fst |> Seq.map snd |> Seq.toList - let expected = [ Started, key; Started, key; Finished, key ] - - Assert.Equal<_ list>(expected, orderedLog) - } - -// [] - if we decide to enable that -let ``Job keeps running if the first requestor cancels`` () = - task { - let jobStarted = new ManualResetEvent(false) + let jobCanComplete = new ManualResetEvent(false) let computation key = node { jobStarted.Set() |> ignore - - for _ in 1 .. 5 do - do! Async.Sleep 100 |> NodeCode.AwaitAsync - + waitFor jobCanComplete return key * 2 } - let eventLog = ConcurrentBag() - let memoize = AsyncMemoize() - memoize.OnEvent(fun (e, (_label, k, _version)) -> eventLog.Add (DateTime.Now.Ticks, (e, k))) + let eventLog = ConcurrentStack() + let memoize = AsyncMemoize() + memoize.OnEvent(fun (e, (_, k, _version)) -> eventLog.Push (e, k)) use cts1 = new CancellationTokenSource() use cts2 = new CancellationTokenSource() @@ -188,25 +135,26 @@ let ``Job keeps running if the first requestor cancels`` () = let key = 1 let _task1 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts1.Token) - jobStarted.WaitOne() |> ignore + + waitFor jobStarted + jobStarted.Reset() |> ignore let _task2 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts2.Token) let _task3 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts3.Token) - jobStarted.WaitOne() |> ignore - cts1.Cancel() - do! Task.Delay 100 + waitFor jobStarted + cts3.Cancel() + jobCanComplete.Set() |> ignore + let! result = _task2 Assert.Equal(2, result) - Assert.Equal(TaskStatus.Canceled, _task1.Status) - - let orderedLog = eventLog |> Seq.sortBy fst |> Seq.map snd |> Seq.toList - let expected = [ Started, key; Finished, key ] + let orderedLog = eventLog |> Seq.rev |> Seq.toList + let expected = [ Started, key; Started, key; Finished, key ] Assert.Equal<_ list>(expected, orderedLog) } @@ -216,18 +164,17 @@ let ``Job is restarted if first requestor cancels but keeps running if second re task { let jobStarted = new ManualResetEvent(false) + let jobCanComplete = new ManualResetEvent(false) + let computation key = node { jobStarted.Set() |> ignore - - for _ in 1 .. 5 do - do! Async.Sleep 100 |> NodeCode.AwaitAsync - + waitFor jobCanComplete return key * 2 } - let eventLog = ConcurrentBag() - let memoize = AsyncMemoize() - memoize.OnEvent(fun (e, (_label, k, _version)) -> eventLog.Add (DateTime.Now.Ticks, (e, k))) + let eventLog = ConcurrentStack() + let memoize = AsyncMemoize() + memoize.OnEvent(fun (e, (_label, k, _version)) -> eventLog.Push (e, k)) use cts1 = new CancellationTokenSource() use cts2 = new CancellationTokenSource() @@ -239,6 +186,7 @@ let ``Job is restarted if first requestor cancels but keeps running if second re jobStarted.WaitOne() |> ignore jobStarted.Reset() |> ignore + let _task2 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts2.Token) let _task3 = NodeCode.StartAsTask_ForTesting( memoize.Get'(key, computation key), ct = cts3.Token) @@ -248,12 +196,12 @@ let ``Job is restarted if first requestor cancels but keeps running if second re cts2.Cancel() + jobCanComplete.Set() |> ignore + let! result = _task3 Assert.Equal(2, result) - Assert.Equal(TaskStatus.Canceled, _task1.Status) - - let orderedLog = eventLog |> Seq.sortBy fst |> Seq.map snd |> Seq.toList + let orderedLog = eventLog |> Seq.rev |> Seq.toList let expected = [ Started, key; Started, key; Finished, key ] Assert.Equal<_ list>(expected, orderedLog) @@ -393,11 +341,21 @@ let ``Cancel running jobs with the same key`` cancelDuplicate expectFinished = let mutable started = 0 let mutable finished = 0 - let work () = node { + let job1started = new ManualResetEvent(false) + let job1finished = new ManualResetEvent(false) + + let jobCanContinue = new ManualResetEvent(false) + + let job2started = new ManualResetEvent(false) + let job2finished = new ManualResetEvent(false) + + let work onStart onFinish = node { Interlocked.Increment &started |> ignore - for _ in 1..10 do - do! Async.Sleep 10 |> NodeCode.AwaitAsync + onStart() |> ignore + waitFor jobCanContinue + do! spinFor (TimeSpan.FromMilliseconds 100) Interlocked.Increment &finished |> ignore + onFinish() |> ignore } let key1 = @@ -406,9 +364,9 @@ let ``Cancel running jobs with the same key`` cancelDuplicate expectFinished = member _.GetVersion() = 1 member _.GetLabel() = "key1" } - cache.Get(key1, work()) |> Async.AwaitNodeCode |> Async.Start + cache.Get(key1, work job1started.Set job1finished.Set) |> Async.AwaitNodeCode |> Async.Start - do! Task.Delay 50 + waitFor job1started let key2 = { new ICacheKey<_, _> with @@ -416,9 +374,16 @@ let ``Cancel running jobs with the same key`` cancelDuplicate expectFinished = member _.GetVersion() = key1.GetVersion() + 1 member _.GetLabel() = "key2" } - cache.Get(key2, work()) |> Async.AwaitNodeCode |> Async.Start + cache.Get(key2, work job2started.Set job2finished.Set ) |> Async.AwaitNodeCode |> Async.Start + + waitFor job2started - do! Task.Delay 500 + jobCanContinue.Set() |> ignore + + waitFor job2finished + + if not cancelDuplicate then + waitFor job1finished Assert.Equal((2, expectFinished), (started, finished)) } diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs index a6b7f6fcc41..57a9e266fe8 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs @@ -219,13 +219,13 @@ let ``Changes in a referenced project`` () = [] let ``File is not checked twice`` () = - let cacheEvents = ResizeArray() + let cacheEvents = ConcurrentQueue() testWorkflow() { withChecker (fun checker -> async { do! Async.Sleep 50 // wait for events from initial project check - checker.Caches.TcIntermediate.OnEvent cacheEvents.Add + checker.Caches.TcIntermediate.OnEvent cacheEvents.Enqueue }) updateFile "First" updatePublicSurface checkFile "Third" expectOk @@ -242,13 +242,13 @@ let ``File is not checked twice`` () = [] let ``If a file is checked as a dependency it's not re-checked later`` () = - let cacheEvents = ResizeArray() + let cacheEvents = ConcurrentQueue() testWorkflow() { withChecker (fun checker -> async { do! Async.Sleep 50 // wait for events from initial project check - checker.Caches.TcIntermediate.OnEvent cacheEvents.Add + checker.Caches.TcIntermediate.OnEvent cacheEvents.Enqueue }) updateFile "First" updatePublicSurface checkFile "Last" expectOk @@ -272,13 +272,13 @@ let ``We don't check files that are not depended on`` () = sourceFile "Third" ["First"], sourceFile "Last" ["Third"]) - let cacheEvents = ResizeArray() + let cacheEvents = ConcurrentQueue() ProjectWorkflowBuilder(project, useTransparentCompiler = true) { withChecker (fun checker -> async { do! Async.Sleep 50 // wait for events from initial project check - checker.Caches.TcIntermediate.OnEvent cacheEvents.Add + checker.Caches.TcIntermediate.OnEvent cacheEvents.Enqueue }) updateFile "First" updatePublicSurface checkFile "Last" expectOk @@ -302,8 +302,8 @@ let ``Files that are not depended on don't invalidate cache`` () = sourceFile "Third" ["First"], sourceFile "Last" ["Third"]) - let cacheTcIntermediateEvents = ResizeArray() - let cacheGraphConstructionEvents = ResizeArray() + let cacheTcIntermediateEvents = ConcurrentQueue() + let cacheGraphConstructionEvents = ConcurrentQueue() ProjectWorkflowBuilder(project, useTransparentCompiler = true) { updateFile "First" updatePublicSurface @@ -311,8 +311,8 @@ let ``Files that are not depended on don't invalidate cache`` () = withChecker (fun checker -> async { do! Async.Sleep 50 // wait for events from initial project check - checker.Caches.TcIntermediate.OnEvent cacheTcIntermediateEvents.Add - checker.Caches.DependencyGraph.OnEvent cacheGraphConstructionEvents.Add + checker.Caches.TcIntermediate.OnEvent cacheTcIntermediateEvents.Enqueue + checker.Caches.DependencyGraph.OnEvent cacheGraphConstructionEvents.Enqueue }) updateFile "Second" updatePublicSurface @@ -344,8 +344,8 @@ let ``Files that are not depended on don't invalidate cache part 2`` () = sourceFile "D" ["B"; "C"], sourceFile "E" ["C"]) - let cacheTcIntermediateEvents = ResizeArray() - let cacheGraphConstructionEvents = ResizeArray() + let cacheTcIntermediateEvents = ConcurrentQueue() + let cacheGraphConstructionEvents = ConcurrentQueue() ProjectWorkflowBuilder(project, useTransparentCompiler = true) { updateFile "A" updatePublicSurface @@ -353,8 +353,8 @@ let ``Files that are not depended on don't invalidate cache part 2`` () = withChecker (fun checker -> async { do! Async.Sleep 50 // wait for events from initial project check - checker.Caches.TcIntermediate.OnEvent cacheTcIntermediateEvents.Add - checker.Caches.DependencyGraph.OnEvent cacheGraphConstructionEvents.Add + checker.Caches.TcIntermediate.OnEvent cacheTcIntermediateEvents.Enqueue + checker.Caches.DependencyGraph.OnEvent cacheGraphConstructionEvents.Enqueue }) updateFile "B" updatePublicSurface checkFile "E" expectOk @@ -382,7 +382,7 @@ let ``Changing impl files doesn't invalidate cache when they have signatures`` ( { sourceFile "B" ["A"] with SignatureFile = AutoGenerated }, { sourceFile "C" ["B"] with SignatureFile = AutoGenerated }) - let cacheEvents = ResizeArray() + let cacheEvents = ConcurrentQueue() ProjectWorkflowBuilder(project, useTransparentCompiler = true) { updateFile "A" updatePublicSurface @@ -390,7 +390,7 @@ let ``Changing impl files doesn't invalidate cache when they have signatures`` ( withChecker (fun checker -> async { do! Async.Sleep 50 // wait for events from initial project check - checker.Caches.TcIntermediate.OnEvent cacheEvents.Add + checker.Caches.TcIntermediate.OnEvent cacheEvents.Enqueue }) updateFile "A" updateInternal checkFile "C" expectOk @@ -412,14 +412,14 @@ let ``Changing impl file doesn't invalidate an in-memory referenced project`` () SyntheticProject.Create("project", sourceFile "B" ["A"] ) with DependsOn = [library] } - let cacheEvents = ResizeArray() + let cacheEvents = ConcurrentQueue() ProjectWorkflowBuilder(project, useTransparentCompiler = true) { checkFile "B" expectOk withChecker (fun checker -> async { do! Async.Sleep 50 // wait for events from initial project check - checker.Caches.TcIntermediate.OnEvent cacheEvents.Add + checker.Caches.TcIntermediate.OnEvent cacheEvents.Enqueue }) updateFile "A" updateInternal checkFile "B" expectOk