diff --git a/src/Equinox.Core/AsyncCacheCell.fs b/src/Equinox.Core/AsyncCacheCell.fs index e2625f86e..caeafcf38 100755 --- a/src/Equinox.Core/AsyncCacheCell.fs +++ b/src/Equinox.Core/AsyncCacheCell.fs @@ -6,26 +6,34 @@ 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` 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 - - let t = task.Value + 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.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 + 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 } /// Synchronously check whether the value has been computed (and/or remains valid) - member x.IsValid(isExpired) = x.TryAwaitValid isExpired |> ValueOption.isSome + member x.IsValid(isExpired) = + if not workflow.IsValueCreated then false else + + let t = workflow.Value + if t = null || not t.IsCompleted || t.IsFaulted then false else + + match isExpired with + | ValueSome f -> not (f t.Result) + | _ -> true /// 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 @@ -33,16 +41,16 @@ type AsyncLazy<'T>(workflow : unit -> Task<'T>) = 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 @@ -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() }