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

Add AsyncLazy<T>.SuppressRelevance() method #1254

Merged
merged 1 commit into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions src/Microsoft.VisualStudio.Threading/AsyncLazy`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,65 @@ public T GetValue(CancellationToken cancellationToken)
}
}

/// <summary>
/// Marks the code that follows as irrelevant to the receiving <see cref="AsyncLazy{T}"/> value factory.
/// </summary>
/// <returns>A value to dispose of to restore relevance into the value factory.</returns>
/// <remarks>
/// <para>In some cases asynchronous work may be spun off inside a value factory.
/// When the value factory does <em>not</em> require this work to finish before the value factory can complete,
/// it can be useful to use this method to mark that code as irrelevant to the value factory.
/// In particular, this can be necessary when the spun off task may actually include code that may itself
/// await the completion of the value factory itself.
/// Such a situation would lead to an <see cref="InvalidOperationException"/> being thrown from
/// <see cref="GetValueAsync(CancellationToken)"/> if the value factory has not completed already,
/// which can introduce non-determinstic failures in the program.</para>
/// <para>A <c>using</c> block around the spun off code can help your program achieve reliable behavior, as shown below.</para>
/// <example>
/// <code><![CDATA[
/// class MyClass {
/// private readonly AsyncLazy<int> numberOfApples;
///
/// public MyClass() {
/// this.numberOfApples = new AsyncLazy<int>(async delegate {
/// // We have some fire-and-forget code to run.
/// // This is *not* relevant to the value factory, which is allowed to complete without waiting for this code to finish.
/// using (this.numberOfApples.SuppressRelevance()) {
/// this.FireOffNotificationsAsync();
/// }
///
/// // This code is relevant to the value factory, and must complete before the value factory can complete.
/// return await this.CountNumberOfApplesAsync();
/// });
/// }
///
/// public event EventHandler? ApplesCountingHasBegun;
///
/// public async Task<int> GetApplesCountAsync(CancellationToken cancellationToken) {
/// return await this.numberOfApples.GetValueAsync(cancellationToken);
/// }
///
/// private async Task<int> CountNumberOfApplesAsync() {
/// await Task.Delay(1000);
/// return 5;
/// }
///
/// private async Task FireOffNotificationsAsync() {
/// // This may call to 3rd party code, which may happen to call back into GetApplesCountAsync (and thus into our AsyncLazy instance),
/// // but such calls should *not* be interpreted as value factory reentrancy. They should just wait for the value factory to finish.
/// // We accomplish this by suppressing relevance of the value factory while this code runs (see the caller of this method above).
/// this.ApplesCountingHasBegun?.Invoke(this, EventArgs.Empty);
/// }
/// }
/// ]]></code>
/// </example>
/// <para>If the <see cref="AsyncLazy{T}"/> was created with a <see cref="JoinableTaskFactory"/>,
/// this method also calls <see cref="JoinableTaskContext.SuppressRelevance"/> on the <see cref="JoinableTaskFactory.Context"/>
/// associated with that factory.
/// </para>
/// </remarks>
public RevertRelevance SuppressRelevance() => new RevertRelevance(this);

/// <summary>
/// Disposes of the lazily-initialized value if disposable, and causes all subsequent attempts to obtain the value to fail.
/// </summary>
Expand Down Expand Up @@ -373,4 +432,40 @@ public override string ToString()
? (this.value.Status == TaskStatus.RanToCompletion ? $"{this.value.Result}" : Strings.LazyValueFaulted)
: Strings.LazyValueNotCreated;
}

/// <summary>
/// A structure that hides relevance of a block of code from a particular <see cref="AsyncLazy{T}"/> and the <see cref="JoinableTaskContext"/> it was created with.
/// </summary>
public readonly struct RevertRelevance : IDisposable
{
private readonly AsyncLazy<T>? owner;
private readonly object? oldCheckValue;
private readonly JoinableTaskContext.RevertRelevance? joinableRelevance;

/// <summary>
/// Initializes a new instance of the <see cref="RevertRelevance"/> struct.
/// </summary>
/// <param name="owner">The instance that created this value.</param>
internal RevertRelevance(AsyncLazy<T> owner)
{
Requires.NotNull(owner, nameof(owner));
this.owner = owner;

(this.oldCheckValue, owner.recursiveFactoryCheck.Value) = (owner.recursiveFactoryCheck.Value, null);
this.joinableRelevance = owner.jobFactory?.Context.SuppressRelevance();
}

/// <summary>
/// Reverts the async local and thread static values to their original values.
/// </summary>
public void Dispose()
{
if (this.owner is object)
{
this.owner.recursiveFactoryCheck.Value = this.oldCheckValue;
}

this.joinableRelevance?.Dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task<Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser?>!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValue() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValueAsync() -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance.Dispose() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task<Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser?>!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValue() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValueAsync() -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance.Dispose() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task<Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser?>!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValue() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValueAsync() -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance.Dispose() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.Name.get -> string!
Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.TryEnterAsync(System.TimeSpan timeout) -> System.Threading.Tasks.Task<Microsoft.VisualStudio.Threading.AsyncCrossProcessMutex.LockReleaser?>!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValue() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.DisposeValueAsync() -> System.Threading.Tasks.Task!
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Microsoft.VisualStudio.Threading.AsyncLazy<T>.IsValueDisposed.get -> bool
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance.Dispose() -> void
Microsoft.VisualStudio.Threading.AsyncLazy<T>.SuppressRelevance() -> Microsoft.VisualStudio.Threading.AsyncLazy<T>.RevertRelevance
96 changes: 96 additions & 0 deletions test/Microsoft.VisualStudio.Threading.Tests/AsyncLazyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,102 @@ public async Task ExecutionContextFlowsFromFirstCaller_JTF()
await asyncLazy.GetValueAsync();
}

[Fact]
public async Task SuppressRelevance_WithoutJTF()
{
AsyncManualResetEvent allowValueFactoryToFinish = new();
Task<int>? fireAndForgetTask = null;
AsyncLazy<int> asyncLazy = null!;
asyncLazy = new AsyncLazy<int>(
async delegate
{
using (asyncLazy.SuppressRelevance())
{
fireAndForgetTask = FireAndForgetCodeAsync();
}

await allowValueFactoryToFinish;
return 1;
},
null);

bool fireAndForgetCodeAsyncEntered = false;
Task<int> lazyValue = asyncLazy.GetValueAsync();
Assert.True(fireAndForgetCodeAsyncEntered);
allowValueFactoryToFinish.Set();

// Assert that the value factory was allowed to finish.
Assert.Equal(1, await lazyValue.WithCancellation(this.TimeoutToken));

// Assert that the fire-and-forget task was allowed to finish and did so without throwing.
Assert.Equal(1, await fireAndForgetTask!.WithCancellation(this.TimeoutToken));

async Task<int> FireAndForgetCodeAsync()
{
fireAndForgetCodeAsyncEntered = true;
return await asyncLazy.GetValueAsync();
}
}

[Fact]
public async Task SuppressRelevance_WithJTF()
{
JoinableTaskContext? context = this.InitializeJTCAndSC();
SingleThreadedTestSynchronizationContext.IFrame frame = SingleThreadedTestSynchronizationContext.NewFrame();

JoinableTaskFactory? jtf = context.Factory;
AsyncManualResetEvent allowValueFactoryToFinish = new();
Task<int>? fireAndForgetTask = null;
AsyncLazy<int> asyncLazy = null!;
asyncLazy = new AsyncLazy<int>(
async delegate
{
using (asyncLazy.SuppressRelevance())
{
fireAndForgetTask = FireAndForgetCodeAsync();
}

await allowValueFactoryToFinish;
return 1;
},
jtf);

bool fireAndForgetCodeAsyncEntered = false;
bool fireAndForgetCodeAsyncReachedUIThread = false;
jtf.Run(async delegate
{
Task<int> lazyValue = asyncLazy.GetValueAsync();
Assert.True(fireAndForgetCodeAsyncEntered);
await Task.Delay(AsyncDelay);
Assert.False(fireAndForgetCodeAsyncReachedUIThread);
allowValueFactoryToFinish.Set();

// Assert that the value factory was allowed to finish.
Assert.Equal(1, await lazyValue.WithCancellation(this.TimeoutToken));
});

// Run a main thread pump so the fire-and-forget task can finish.
SingleThreadedTestSynchronizationContext.PushFrame(SynchronizationContext.Current!, frame);

// Assert that the fire-and-forget task was allowed to finish and did so without throwing.
Assert.Equal(1, await fireAndForgetTask!.WithCancellation(this.TimeoutToken));

async Task<int> FireAndForgetCodeAsync()
{
fireAndForgetCodeAsyncEntered = true;

// Yield the caller's thread.
// Resuming will require the main thread, since the caller was on the main thread.
await Task.Yield();

fireAndForgetCodeAsyncReachedUIThread = true;

int result = await asyncLazy.GetValueAsync();
frame.Continue = false;
return result;
}
}

[Fact]
public async Task Dispose_ValueType_Completed()
{
Expand Down