-
Notifications
You must be signed in to change notification settings - Fork 787
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor AsyncMemoize, introduce AsyncLazy #18002
base: main
Are you sure you want to change the base?
Conversation
aef4303
to
00a5602
Compare
|
89cbde6
to
e1acf39
Compare
Now, that rings a bell. I'll make sure it still works. |
Not yet, but we plan to start using it when it's considered stable/recommended. |
@auduchinok, this change here should affect only TransparentCompiler, nothing else. OCEs originating from compiler tasks cached by AsyncMemoize should propagate like any other exception, without special treatment. I think this should be ok in this regard. |
But we never want to cache OCEs, do we? Or is it less relevant with the transparent compiler, because the change in files will invalidate things in a natural way? |
I'm not super familiar with this part, but IIUC AsyncMemoize is keyed by partial project snapshots. Ideally a snapshot should encode all of the deps, such as to make AsyncMemoize.Get a pure function. In practice many things can happen, so we should probably evict exceptional results pretty quickly from the cache. Again, I still don't know much about it, I don't want to spout nonsense :) |
Cancellable does not throw OCE on the async boundary, it just calls cancel continuation, very neat: fsharp/src/Compiler/Utilities/Cancellable.fs Lines 72 to 82 in fb69e58
|
c8bc3e7
to
6fbfa44
Compare
c78c4e6
to
34c8640
Compare
Nice! Can we run some benchmarks of before and after to see if anything changed? (hopefully improved)
I haven't studied this yet, but intuitively I would say OCEs might deserve special treatment. User might open a file, checking starts, they decide to close it so it gets cancelled. Then without changing anything that affects it they open it again. Same snapshot should be used and we need to restart the computation. We should probably still restart even in case of a failed job, but there theoretically we could decide not to, so we should be able to tell the difference between failed and cancelled. |
How does it look with stack traces here? Are we able to preserve them over the request? Would be nice if we could, though I didn't figure it out before so if not it's not a downgrade... |
It is possible to store exception in fsharp/src/FSharp.Core/async.fs Line 44 in 3dff60a
if you catch an exception in async CE and then rethrow it in another async, it should restore the stacktrace from ExceptionDispatchInfo cached in that ConditionalWeakTable .
I think we need a test case just for this. It would allow for some experiments. |
This is kind of what this PR already do: Failure is a failure - currently it's just cached as (Result.Error(exn), logger) value. Cancellation happens when the client cancels the request. We detach from the job and it is configurable whether is runs to completion unawaited or is itself canceled. What is still unclear to me is the situation with TBH I already built a vsix out of this and I'm using it for some time. Never saw any exceptions so I hope this is a non-issue. On the plus side, I had codefix analyzers throwing each time I switched branches in VS and with this PR this never happens, although it may be unrelated. |
Previously, the stack trace was always cut by a jump to the thread pool. You could always only see up to the point where a worker thread picked it up from there.
That sounds promising! Maybe regular operation is already covered and any other OCEs are actual errors, i.e. they shouldn't have been thrown. However, there is always some risk that some future changes will introduce an issue by throwing somewhere we don't expect it and by then no one will remember this 😂 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks much better than before!
Co-authored-by: Petr Pokorny <petr@innit.cz>
…into refactor-asyncmemo
Something to investigate. Script load closure tests don't like this version for some reason: member _.TryGet(key: 'TKey, predicate: 'TVersion -> bool) : 'TValue option =
lock cache <| fun () ->
cache.GetAll(key)
|> Seq.tryPick (fun (version, job) ->
match predicate version, job.TryResult with
| true, Some(Ok result, _) -> Some result
| _ -> None) I had to take a snapshot with toList and do tryPick outside of the lock instead. |
That's how I thought it should work, i.e. we should not cache the cancellations and recalculate it in future if needed, even on the same snapshots. I hope we get more
@majocha Is there any chance you could add tests covering this behavior if there aren't any already? |
Yes, this would be great! Currently, because of this code: fsharp/src/Compiler/Utilities/Cancellable.fs Lines 9 to 11 in a95a9ea
If there is no ambient Cancellable, |
Added back | Completed of result: 't
| Faulted of exn to the state machine. Also basic optimizations: computation and task will deallocate on completion and only the result is stored. |
@majocha - great job here (as usual), also thanks for a solid PR description and GH gist, it helps reviewing things a lot. The more tests the better here, but this can be done as a follow up - as you wish. |
Maintainability is the goal here. The code is smaller now for sure but that not necessarily implies more understandable, so let's see how it goes.
The state machine of running / tracking requests / canceling jobs is now moved to a separate
AsyncLazy
type.This is not just a refactor because cancellation behavior changed:
A running job that is awaited by a client will never get canceled "from the inside" - from the requestor's pov it'd be just an exception thrown from the job, so this is not allowed.
Completion behavior is configurable now. If all clients cancel their requests,
AsyncLazy
can either cancel the job or follow to completion and store result anyway. This may or may not be useful.The LRU cache now just holds
AsyncLazy
values. State transitions happen inside theAsyncLazy
so there is no correspondingcache.Set
like before.Gist of the file for easier reading is here: https://gist.github.com/majocha/6c075dcdabf45c3fd92b03e2cf3a07eb
Todo:
Investigate passing
ExceptionDispatchInfo
from the job instead of reraising exns.