Skip to content
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

Performance: Tasks / Struct #337

Merged
merged 14 commits into from
Sep 2, 2022
Prev Previous commit
Next Next commit
AsyncCacheCell wip
  • Loading branch information
bartelink committed Sep 1, 2022
commit 6e4e82c83ef9a12f47c822d8fae7ddd46dd8ca9d
44 changes: 26 additions & 18 deletions src/Equinox.Core/AsyncCacheCell.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,51 @@ open System.Threading.Tasks
/// Asynchronous Lazy<'T> used to gate a workflow to ensure at most once execution of a computation.
type AsyncLazy<'T>(workflow : unit -> Task<'T>) =

let task = Lazy.Create workflow
let workflow = lazy workflow ()

/// Await the outcome of the computation.
/// NOTE due to `Lazy<T>` semantics, failed attempts will cache any exception; AsyncCacheCell compensates for this
member _.Await() = task.Value
member _.Await() = workflow.Value

/// Used to rule out values where the computation yielded an exception or the result has now expired
member _.TryAwaitValid(isExpired) : 'T voption =
if not task.IsValueCreated then ValueNone else
/// Synchronously check whether the value has been computed (and/or remains valid)
member x.IsValid(isExpired) =
if not workflow.IsValueCreated then false else

let t = task.Value
let t = workflow.Value
if t = null || not t.IsCompleted || t.IsFaulted then false else

// Determines if the last attempt completed, but failed; For TMI see https://stackoverflow.com/a/33946166/11635
if t.Status <> TaskStatus.RanToCompletion then ValueNone // net6.0 brings an IsCompletedSuccessfully, but we're still netstandard
else match isExpired with
| ValueSome check when not (check t.Result) -> ValueNone
| _ -> ValueSome t.Result
match isExpired with
| ValueSome f -> not (f t.Result)
| _ -> true

/// Synchronously check whether the value has been computed (and/or remains valid)
member x.IsValid(isExpired) = x.TryAwaitValid isExpired |> ValueOption.isSome
/// Used to rule out values where the computation yielded an exception or the result has now expired
member _.TryAwaitValid(isExpired) : Task<'T voption> =
let t = workflow.Value

// Determines if the last attempt completed, but failed; For TMI see https://stackoverflow.com/a/33946166/11635
if t = null || t.IsFaulted then Task.FromResult ValueNone
else task {
let! (res : 'T) = t
match isExpired with
| ValueSome check when not (check res) -> return ValueNone
| _ -> return ValueSome res }

/// Generic async lazy caching implementation that admits expiration/recomputation/retry on exception semantics.
/// If `workflow` fails, all readers entering while the load/refresh is in progress will share the failure
/// The first caller through the gate triggers a recomputation attempt if the previous attempt ended in failure
type AsyncCacheCell<'T>(workflow : CancellationToken -> Task<'T>, ?isExpired : 'T -> bool) =

let isExpired = match isExpired with Some x -> ValueSome x | None -> ValueNone
let mutable cell = AsyncLazy(fun () -> Task.FromCanceled<_>(CancellationToken.None))
let mutable cell = AsyncLazy(fun () -> null)

/// Synchronously check the value remains valid (to short-circuit an Async AwaitValue step where value not required)
member _.IsValid() = cell.IsValid(isExpired)
/// Gets or asynchronously recomputes a cached value depending on expiry and availability
member _.Await(ct) =
member _.Await(ct) = task {
// First, take a local copy of the current state
let current = cell
match current.TryAwaitValid(isExpired) with
| ValueSome res -> Task.FromResult res // ... if it's already / still valid, we're done
match! current.TryAwaitValid(isExpired) with
| ValueSome res -> return res // ... if it's already / still valid, we're done
| ValueNone ->
// Prepare to do the work, with cancellation under out control
let attemptLoad () = workflow ct
Expand All @@ -51,4 +59,4 @@ type AsyncCacheCell<'T>(workflow : CancellationToken -> Task<'T>, ?isExpired : '
// avoid unnecessary recomputation in cases where competing threads detect expiry;
// the first write attempt wins, and everybody else reads off that value
let _ = Interlocked.CompareExchange(&cell, AsyncLazy(dispatch), current)
cell.Await()
return! cell.Await() }