-
-
Notifications
You must be signed in to change notification settings - Fork 837
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
Ensure that IAsyncDisposable instances that have not been activated are correctly disposed when the scope is disposed. #1285
Conversation
…e instances when we dispose of scopes.
Codecov Report
@@ Coverage Diff @@
## develop #1285 +/- ##
===========================================
+ Coverage 76.54% 76.64% +0.09%
===========================================
Files 188 188
Lines 5125 5163 +38
Branches 1042 1057 +15
===========================================
+ Hits 3923 3957 +34
- Misses 707 708 +1
- Partials 495 498 +3
Continue to review full report at Codecov.
|
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.
No joke on the viral nature of async stuff! Looks pretty good, a couple of minor comments/questions.
{ | ||
// Type only implements IAsyncDisposable, which is not valid if there | ||
// is a synchronous dispose being done. | ||
throw new InvalidOperationException(string.Format( |
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 somewhat torn here. I'm curious if we should throw or just do like a DisposeAsync().GetAwaiter().Wait()
. Or maybe nothing?
I'm not sure about throwing during Dispose
, like how WCF would do if a network connection dropped and we ended up having to work around that with a special OnRelease
handler. I know I'm using ServiceBusClient
, which is only IAsyncDisposable
, in some of our code and an exception here would be both unexpected and possibly not easy to fix, especially if the container is getting disposed by something outside my control during app shutdown.
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.
So, this "throw if you're service isn't going to be disposable" was the approach we took for other service types when we added IAsyncDisposable support.
I suppose the problematic scenario is the one where you aren't implementing the component, like ServiceBusClient
, so can't remedy the problem.
We can do the GetAwaiter().Wait()
, it just means that user's might not be getting the nice async dispose they think they're getting. I too am torn.
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.
Yeah, I didn't think the exception was going to be an issue until I hit this ServiceBusClient
thing where it's causing all sorts of weird sync/async disposal problems because they didn't implement both. I have to imagine there are other cases like this out there, I just happen to find a concrete pain in my ass right here so I'm latching onto it.
I guess it comes down to: would it be better to dispose synchronously or not dispose at all? And if it doesn't dispose (and throws), it looks like it stops everything else from being disposed, too. Which I'll admit sounded right at the time but IAsyncDisposable
was kind of new and I didn't really grok it like I'm starting to now.
It's almost like it should be a warning rather than an exception. Not that that's a thing, just like, "Hey, you did something wrong and we'll recover for you by doing it synchronously, but you're not getting the nice async you think you are."
Barring that... my gut is telling me we should do what we can to dispose without throwing.
Another option might be to require folks to opt in with something like
builder.RegisterType<ServiceBusClient>().SynchronousDisposal();
where SynchronousDisposal
could be an OnRelease
thing sort of like the WCF SafeRelease
extension where it wraps DisposeAsync().GetAwaiter().Wait()
logic.
What's the "pit of success" here? What will make people more readily successful with the least amount of client work? Successful by default?
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.
Minor addition, we would need to AsTask()
on the result of DisposeAsync
, you cannot GetAwaiter()
on a ValueTask
. Only worth mentioning as another allocation that would happen during dispose.
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.
Like you say, raising a warning would be nice. I think the pit of success is "probably" GetAwaiter().Wait()
, but perhaps we could write something somewhere, like to Debug.Trace
?
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'd go for a Debug.WriteLine
or something similar. Maybe Trace
instead of Debug
- doesn't Debug
get compiled out in release mode? I vaguely recall that being a thing. I could see possibly raising an event with the diagnostic listener thing for issues like this, too, maybe... but that'd mean propagating it around down to this level and yeahno.
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.
Ok, this is done; used Trace.TraceWarning
. Attached tracers understand it's a warning level, which is good.
src/Autofac/Core/Activators/ProvidedInstance/ProvidedInstanceActivator.cs
Show resolved
Hide resolved
test/Autofac.Specification.Test/Lifetime/AsyncDisposeProvidedInstanceTests.cs
Show resolved
Hide resolved
…but with a Trace warning.
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.
📱
It looks like the build is hanging on the unit tests in |
I can't replicate the hang locally. I wonder if updating the |
I'll stop the build and change the global.json. |
That same path separator problem needed applying to the main Autofac repo, so including it in this PR. |
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.
🖱️
Still hanging. These deadlock issues remind me of one of the reasons why I didn't do this sync-over-async in the first place. https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html I tried Worth noting that a deadlock is far harder to understand/debug than an exception.... |
Interestingly only hangs on net5; perhaps some threading thing has been changed. |
Hmmmm. Agreed that exception is easier to troubleshoot. I also have to admit I don't really understand why disposal needs to be async at all but OOOOOKAY. Waiting on async constructors now because that'll be the best idea EVAR. 😩 I think my biggest issue with the exception was that we didn't allow stuff to be disposed that could be disposed before throwing. On the other hand, if I wanted to bridge the gap and put some sort of extension that allows sync disposal of an only async component, I'd still be stuck with this issue and it'd be entirely outside my control - thing X ( |
🐔 <-> 🥚 |
Async disposal is useful when you want compact cleanup of remote resources. E.g.
Sort of besides the point though, IAsyncDisposable is here to stay, so we'd better support it. I'll take a look and see what else we can do here. |
OK; fixed the deadlock via the somewhat nuclear option of bumping the Disposal of IAsyncDisposable-only objects onto the threadpool and waiting for completion, to ensure we get a completely fresh async context. Basically, if I just call |
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 can't think of anything better. Luckily it should be very rare. Nice work figuring it out! |
This change was slightly larger than expected; due to the somewhat viral nature of async code, I had to add
DisposeAsync
methods in a number of places.This necessitated the addition of
IAsyncDisposable
as a base interface on some of our public types. However, the types modified areIComponentRegistration
andIComponentRegistry
, which fall into the category of "things that people probably won't implement themselves."I avoided a breaking change on
IInstanceActivator
(perhaps more likely to be implemented) by checking if it's an IAsyncDisposable before continuing.