-
Notifications
You must be signed in to change notification settings - Fork 154
Add a JTF-derived type that cancels a DisposalToken and drains the JoinableTaskCollection when disposed #1539
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
base: main
Are you sure you want to change the base?
Changes from all commits
30049eb
24f0278
fc75d71
121be2a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
|
||
| using System; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace Microsoft.VisualStudio.Threading; | ||
|
|
||
| /// <summary> | ||
| /// A variant on <see cref="JoinableTaskFactory"/> that tracks pending tasks and blocks disposal until all tasks have completed. | ||
| /// A cancellation token is provided so pending tasks can cooperatively cancel when disposal is requested. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// Cancellation of pending tasks is cooperative. | ||
| /// If a pending task does not observe <see cref="DisposalToken" />, then disposal may take longer to complete, | ||
| /// or even never complete if a pending task never completes. | ||
| /// </para> | ||
| /// <para> | ||
| /// Creating tasks after disposal has been requested is not prevented by this class. | ||
| /// </para> | ||
| /// </remarks> | ||
| public class DisposableJoinableTaskFactory : DelegatingJoinableTaskFactory, IDisposable, System.IAsyncDisposable | ||
| { | ||
| private readonly CancellationTokenSource disposalTokenSource = new(); | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="DisposableJoinableTaskFactory"/> class. | ||
| /// </summary> | ||
| /// <param name="innerFactory">The factory instance to be wrapped. Must have an associated collection.</param> | ||
| public DisposableJoinableTaskFactory(JoinableTaskFactory innerFactory) | ||
| : base(innerFactory) | ||
| { | ||
| Requires.Argument(this.Collection is not null, nameof(innerFactory), "A collection must be associated with the factory."); | ||
|
|
||
| // Get it now, since after the CTS is disposed, it throws when we try to access the token. | ||
| this.DisposalToken = this.disposalTokenSource.Token; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="DisposableJoinableTaskFactory"/> class. | ||
| /// </summary> | ||
| /// <param name="joinableTaskContext">The <see cref="JoinableTaskContext"/> used to construct the <see cref="JoinableTaskFactory"/>.</param> | ||
| /// <remarks> | ||
| /// This constructor creates a <see cref="JoinableTaskFactory"/> using <see cref="JoinableTaskContext.CreateFactory(JoinableTaskCollection)"/>. | ||
| /// </remarks> | ||
| public DisposableJoinableTaskFactory(JoinableTaskContext joinableTaskContext) | ||
| : this(Requires.NotNull(joinableTaskContext).CreateFactory(Requires.NotNull(joinableTaskContext).CreateCollection())) | ||
| { | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets a disposal token that <em>should</em> be used by tasks created by this factory to know when they should stop doing work. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// This token is canceled when the factory is disposed. | ||
| /// </remarks> | ||
| public CancellationToken DisposalToken { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets the collection to which created tasks belong until they complete. | ||
| /// </summary> | ||
| protected new JoinableTaskCollection Collection => base.Collection!; | ||
|
|
||
| /// <inheritdoc/> | ||
| public void Dispose() | ||
| { | ||
| if (!this.disposalTokenSource.IsCancellationRequested) | ||
| { | ||
| this.disposalTokenSource.Cancel(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code is not thread-safe. When Dispose is called on two threads at the same time, one might run into ObjectDisposedException. The definition of the disposable pattern requires Dispose to be thread-safe and can be called any number of times.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not many classes are thread-safe for disposal. |
||
| this.disposalTokenSource.Dispose(); | ||
| } | ||
|
|
||
| this.Context.Factory.Run(() => this.Collection.JoinTillEmptyAsync()); | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public async ValueTask DisposeAsync() | ||
| { | ||
| if (!this.disposalTokenSource.IsCancellationRequested) | ||
| { | ||
| this.disposalTokenSource.Cancel(); | ||
| this.disposalTokenSource.Dispose(); | ||
| } | ||
|
|
||
| await this.Collection.JoinTillEmptyAsync(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
|
||
| using System; | ||
| using System.Threading.Tasks; | ||
|
|
||
| public class DisposableJoinableTaskFactoryTests : TestBase | ||
| { | ||
| private readonly JoinableTaskContext context; | ||
| private readonly JoinableTaskCollection joinableCollection; | ||
| private readonly JoinableTaskFactory asyncPump; | ||
|
|
||
| public DisposableJoinableTaskFactoryTests(ITestOutputHelper logger) | ||
| : base(logger) | ||
| { | ||
| this.context = JoinableTaskContext.CreateNoOpContext(); | ||
| this.joinableCollection = this.context.CreateCollection(); | ||
| this.asyncPump = this.context.CreateFactory(this.joinableCollection); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void DisposeCancelsToken() | ||
| { | ||
| DisposableJoinableTaskFactory factory = new(this.asyncPump); | ||
|
|
||
| // The collection is already empty, so this completes immediately. | ||
| factory.Dispose(); | ||
|
|
||
| Assert.True(factory.DisposalToken.IsCancellationRequested); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task DisposeAsyncCancelsToken() | ||
| { | ||
| DisposableJoinableTaskFactory factory = new(this.asyncPump); | ||
|
|
||
| // The collection is already empty, so this completes immediately. | ||
| await factory.DisposeAsync(); | ||
|
|
||
| Assert.True(factory.DisposalToken.IsCancellationRequested); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void MultipleDisposalsDoNotThrow() | ||
| { | ||
| using DisposableJoinableTaskFactory factory = new(this.asyncPump); | ||
|
|
||
| factory.Dispose(); | ||
| factory.Dispose(); | ||
|
|
||
| Assert.True(factory.DisposalToken.IsCancellationRequested); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task MultipleDisposeAsyncsDoesNotThrow() | ||
| { | ||
| DisposableJoinableTaskFactory factory = new(this.asyncPump); | ||
|
|
||
| await factory.DisposeAsync(); | ||
| await factory.DisposeAsync(); | ||
|
|
||
| Assert.True(factory.DisposalToken.IsCancellationRequested); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ConstructorRequiresCollection() | ||
| { | ||
| // A factory created with just a context has no collection. | ||
| JoinableTaskFactory factoryWithoutCollection = this.context.Factory; | ||
| Assert.ThrowsAny<Exception>(() => new DisposableJoinableTaskFactory(factoryWithoutCollection)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task TaskObservesCancellationDuringDrain() | ||
| { | ||
| DisposableJoinableTaskFactory factory = new(this.asyncPump); | ||
|
|
||
| JoinableTask jt = factory.RunAsync(() => Task.Delay(UnexpectedTimeout, factory.DisposalToken)); | ||
|
|
||
| await factory.DisposeAsync().AsTask().WithTimeout(UnexpectedTimeout); | ||
| Assert.True(jt.IsCompleted); | ||
| await Assert.ThrowsAsync<TaskCanceledException>(async () => await jt); | ||
| } | ||
| } |
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.
The design is a little odd that this factory wraps an inner factory, and when it is disposed, it waits a JoinableTaskCollection potentially used by others
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.
I'm not sure I agree with that. You can't get a collection out of a factory, so unless the factory creator chose to create and share a collection, no one else could be adding to that collection.
Of course disposal causing a block for pending tasks is odd for this library, but blocking on tasks on disposal isn't at all odd for VS.