-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
68 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,47 +1,51 @@ | ||
namespace Equinox.Core | ||
|
||
/// Asynchronous Lazy<'T> that guarantees workflow will be executed at most once. | ||
/// Asynchronous Lazy<'T> used to gate a workflow to ensure at most once execution of a computation. | ||
type AsyncLazy<'T>(workflow : Async<'T>) = | ||
let task = lazy(Async.StartAsTask workflow) | ||
let task = lazy (Async.StartAsTask workflow) | ||
|
||
/// Await the outcome of the computation. | ||
/// NOTE due to `Lazy<T>` semantics, failed attempts will cache any exception; AsyncCacheCell compensates for this | ||
member __.AwaitValue() = Async.AwaitTaskCorrect task.Value | ||
member internal __.PeekInternalTask = task | ||
|
||
/// Generic async lazy caching implementation that admits expiration/recomputation semantics | ||
/// If `workflow` fails, all readers entering while the load/refresh is in progress will share the failure | ||
type AsyncCacheCell<'T>(workflow : Async<'T>, ?isExpired : 'T -> bool) = | ||
let mutable currentCell = AsyncLazy workflow | ||
/// Synchronously check whether the value has been computed (and/or remains valid) | ||
member this.IsValid(?isExpired) = | ||
if not task.IsValueCreated then false else | ||
|
||
let initializationFailed (value : System.Threading.Tasks.Task<_>) = | ||
// for TMI on this, see https://stackoverflow.com/a/33946166/11635 | ||
value.IsCompleted && value.Status <> System.Threading.Tasks.TaskStatus.RanToCompletion | ||
let value = task.Value | ||
if not value.IsCompleted || value.IsFaulted then false else | ||
|
||
let update cell = async { | ||
// avoid unnecessary recomputation in cases where competing threads detect expiry; | ||
// the first write attempt wins, and everybody else reads off that value | ||
let _ = System.Threading.Interlocked.CompareExchange(¤tCell, AsyncLazy workflow, cell) | ||
return! currentCell.AwaitValue() | ||
} | ||
match isExpired with | ||
| Some f -> not (f value.Result) | ||
| _ -> true | ||
|
||
/// Enables callers to short-circuit the gate by checking whether a value has been computed | ||
member __.PeekIsValid() = | ||
let cell = currentCell | ||
let currentState = cell.PeekInternalTask | ||
if not currentState.IsValueCreated then false else | ||
/// Used to rule out values where the computation yielded an exception or the result has now expired | ||
member this.TryAwaitValid(?isExpired) : Async<'T option> = async { | ||
// Determines if the last attempt completed, but failed; For TMI see https://stackoverflow.com/a/33946166/11635 | ||
if task.Value.IsFaulted then return None else | ||
|
||
let value = currentState.Value | ||
not (initializationFailed value) | ||
&& (match isExpired with Some f -> not (f value.Result) | _ -> false) | ||
let! result = this.AwaitValue() | ||
match isExpired with | ||
| Some f when f result -> return None | ||
| _ -> return Some result | ||
} | ||
|
||
/// 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 : Async<'T>, ?isExpired : 'T -> bool) = | ||
let mutable cell = AsyncLazy workflow | ||
|
||
/// Synchronously check the value remains valid (to short-circuit an Async AwaitValue step where value not required) | ||
member __.IsValid() = cell.IsValid(?isExpired=isExpired) | ||
/// Gets or asynchronously recomputes a cached value depending on expiry and availability | ||
member __.AwaitValue() = async { | ||
let cell = currentCell | ||
let currentState = cell.PeekInternalTask | ||
// If the last attempt completed, but failed, we need to treat it as expired | ||
if currentState.IsValueCreated && initializationFailed currentState.Value then | ||
return! update cell | ||
else | ||
let! current = cell.AwaitValue() | ||
match isExpired with | ||
| Some f when f current -> return! update cell | ||
| _ -> return current | ||
let current = cell | ||
match! current.TryAwaitValid(?isExpired=isExpired) with | ||
| Some res -> return res | ||
| None -> | ||
// avoid unnecessary recomputation in cases where competing threads detect expiry; | ||
// the first write attempt wins, and everybody else reads off that value | ||
let _ = System.Threading.Interlocked.CompareExchange(&cell, AsyncLazy workflow, current) | ||
return! cell.AwaitValue() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters