Skip to content

Add a JTF-derived type that cancels a DisposalToken and drains the JoinableTaskCollection when disposed#1539

Open
vivlimmsft wants to merge 4 commits intomicrosoft:mainfrom
vivlimmsft:dev/vivlim/self-cancelling-jtf
Open

Add a JTF-derived type that cancels a DisposalToken and drains the JoinableTaskCollection when disposed#1539
vivlimmsft wants to merge 4 commits intomicrosoft:mainfrom
vivlimmsft:dev/vivlim/self-cancelling-jtf

Conversation

@vivlimmsft
Copy link
Contributor

  • Add an internal event to JoinableTaskCollection which is raised when JoinTillEmptyAsync is called
  • Add SelfCancellingJoinableTaskFactory, a variant of DelegatingJoinableTaskFactory which contains a CancellationTokenSource and cancels it during shutdown (signalled by that event, or Dispose being called)

The motivation behind this change is that I work in a codebase that has many misuses of JTF, where a cancellation token which is not associated with the lifetime of that JTF is used. I would like to have access to a token that is guaranteed to have been cancelled during JoinTillEmptyAsync, and I think putting that token inside of the JTF will make it easier to pass along and update existing code to use the correct token.

One issue I see with this idea is that call sites that want to make use of the DisposalToken will need to cast or pattern match on the SelfCancellingJoinableTaskFactory type. Would it be appropriate to also add an extension method that handles that?

vivlimmsft and others added 4 commits February 9, 2026 16:52
… called.

- Add an internal event to JoinableTaskCollection which is raised when JoinTillEmptyAsync is called
- Add SelfCancellingJoinableTaskFactory, a variant of DelegatingJoinableTaskFactory which contains a CancellationTokenSource and cancels it during shutdown (signalled by that event, or Dispose being called)
@AArnott
Copy link
Member

AArnott commented Feb 10, 2026

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@AArnott AArnott changed the title Add a JTF with a token that gets cancelled when JoinTillEmptyAsync is called Add a JTF-derived type that cancels a DisposalToken and drains the JoinableTaskCollection when disposed Feb 10, 2026
Copy link
Member

@lifengl lifengl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this could be useful utility wrap within some kinds of usage, but I feel making a special kind of JoinableTaskFactory disposable doesn't fit the overall design pattern very well. The Disposed factory is actually still functional as other caller can still call RunAsync to start a task, with or without taking care of the CancellationToken, and injecting another task to the underline collection.

Overall, a JTF factory may never die, even when all JTF Task spinning off from it are all completed, but any of the child spin off JTF task from them might still be alive, and they would all be backed chained to the JoinableTaskFactory and potentially call its method, so making one disposable is somewhat odd from that logic point of view.

{
if (!this.disposalTokenSource.IsCancellationRequested)
{
this.disposalTokenSource.Cancel();
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not many classes are thread-safe for disposal.
I disagree that the disposable pattern requires thread-safety. It requires that Dispose can be called multiple times, but it doesn't specify that this may happen concurrently.
I agree we could improve this though.

public DisposableJoinableTaskFactory(JoinableTaskFactory innerFactory)
: base(innerFactory)
{
Requires.Argument(this.Collection is not null, nameof(innerFactory), "A collection must be associated with the factory.");
Copy link
Member

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it waits a JoinableTaskCollection potentially used by others

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.

@lifengl
Copy link
Member

lifengl commented Feb 11, 2026

Personally, I will question what is the mis-use cases we talked about. I don't feel generally we have a cancellation token - JoinableTaskCollection alignments. That is specific scenario which things are being misused is mostly inside VS package, where tearing down a package often ends by waiting a JTF collection, and it should use a cancellation token which is being triggered when the package need to be closed. It might be more a problem of the VS package pattern than a pattern to be forced in the JTF level.

@AArnott
Copy link
Member

AArnott commented Feb 12, 2026

Thank you for your review, @lifengl. I share your concern about this being a good fit for the library (even after my changes).
There's nothing here that can't be done by a VS utility class in another assembly, except for looking up the JTF.Collection property in the derived type, since that's an internal property which this PR makes protected internal. If that's important, we could keep that one line in the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants