mayuki / Rin

Request/response Inspector middleware for ASP.NET Core
MIT License
647 stars 24 forks source link

Using Autofac and Rin causes problems on application shutdown #14

Open kevindqc opened 5 years ago

kevindqc commented 5 years ago

Hi! I love Rin! Unfortunately, it seems it doesn't like when used with Autfac.

I uploaded a repro repository here: https://github.com/kevindqc/RinShutdownProblemRepro

The problem happens when I run dotnet run, and after that press CTRL+C. The app doesn't close down properly. On the repro repository, it crashes, but in my actual project, it logs an error and the dotnet processes stay open - if I try to run dotnet run it doesn't work because the files (DLLs) are in use. This slows down development considerably as I have to kill the 4 left-opened dotnet processes constantly.

The exception is:

Unhandled Exception: System.AggregateException: One or more errors occurred. (A task was canceled.) ---> System.Threading.Tasks.TaskCanceledException: A task was canceled.
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
   at Rin.Core.Event.MessageEventBus`1.Dispose()
   at Autofac.Core.Disposer.Dispose(Boolean disposing)
   at Autofac.Util.Disposable.Dispose()
   at Autofac.Core.Lifetime.LifetimeScope.Dispose(Boolean disposing)
   at Autofac.Util.Disposable.Dispose()
   at Autofac.Core.Container.Dispose(Boolean disposing)
   at Autofac.Util.Disposable.Dispose()
   at Autofac.Extensions.DependencyInjection.AutofacServiceProvider.Dispose(Boolean disposing) in C:\projects\autofac-extensions-dependencyinjection\src\Autofac.Extensions.DependencyInjection\AutofacServiceProvider.cs:line 105
   at Autofac.Extensions.DependencyInjection.AutofacServiceProvider.Dispose() in C:\projects\autofac-extensions-dependencyinjection\src\Autofac.Extensions.DependencyInjection\AutofacServiceProvider.cs:line 115
   at Microsoft.AspNetCore.Hosting.Internal.WebHost.Dispose()
   at Microsoft.AspNetCore.Hosting.WebHostExtensions.RunAsync(IWebHost host, CancellationToken token, String shutdownMessage)
   at Microsoft.AspNetCore.Hosting.WebHostExtensions.RunAsync(IWebHost host, CancellationToken token)
   at Microsoft.AspNetCore.Hosting.WebHostExtensions.Run(IWebHost host)
   at RinShutdownProblemRepro.Program.Main(String[] args) in C:\Users\doyonke\source\repos\RinShutdownProblemRepro\RinShutdownProblemRepro\Program.cs:line 18

It seems that Autofac is disposing MessageEventBus, which in its Dispose method waits for a task, but that task has already been canceled. Maybe the dispose order is different when using Autofac vs. using MS Dependency Injection? I made a branch that doesn't use Autofac, and I don't seem to have the problem: https://github.com/kevindqc/RinShutdownProblemRepro/tree/WithoutAutofac

mayuki commented 5 years ago

When using default MS Dependency Injection, it will never call IDisposable.Dispose method of services during shut down. However, Autofac calls that exactly, so that Task throws an exception. I didn't recognize it.

I fixed it to ignore TaskCancelledException during shut down, and published version 1.0.5.

kevindqc commented 5 years ago

@mayuki Awesome, thanks!

Are you sure it doesn't dispose? The documentation says it does in most situations.

If you add a service like this: services.AddSingleton<Service2>(); The container will create the Service2 itself, and take care of disposing it. I tested it and it does dispose.

But if you create the object youself like this: services.AddSingleton<Service3>(new Service3()); then it won't get disposed.

I see you are doing the later:

var eventBus = new MessageEventBus<RequestEventMessage>();
services.AddSingleton<IMessageEventBus<RequestEventMessage>>(eventBus);

Maybe it should be:

services.AddSingleton<IMessageEventBus<RequestEventMessage>, MessageEventBus<RequestEventMessage>>();

That way the dependency injected services will get disposed regardless of the container? But it might cause the problem I still have with Autofac, so it should probably get fixed first lol

I tried 1.0.5, and I don't see the exception being thrown, but I still see a dotnet process outlast shutdown. I updated the repro to 1.0.5. If you run dotnet run, press CTRL+C, a dotnet process is still opened (at least on Windows). I've seen this problem in winforms app caused by a managed thread that is still doing stuff. Not sure about this case, there doesn't seem to be any thread manually created anywhere in the code :/

mayuki commented 5 years ago

@kevindqc Thank you very much for your advice about DI behavior! I understand why injected instances aren't disposed of. sorry, It's my fault.

EventMessageBus instances are independent which can be deleted in any order. Therefore, I will fix it with your suggestion.

Press Ctrl+C to stop dotnet run then the process seems like to exit correctly in the console. However, I can see dotnet exec process which is still running in the background. The process will terminate after waiting for a little.

I guess the HTTP server (Kestrel) is waiting for WebSocket connections to close. For example, Close the browser tab of Rin inspector, wait 5 seconds and press Ctrl+C, the process will terminate immediately.

kevindqc commented 5 years ago

Yeah I thought the same thing, websockets might keep the dotnet process alive, but it happens even I don't have any browser window opened to /rin or any MVC view that has the Rin In-View inspector (it's only a Web API)

Also, if the websockets are the cause of the dotnet process to stay, wouldn't using MS DI have the same problem? Unless you had the DI container create the websockets (so they would get disposed with Autofac, which might cause waiting, but not MS DI). That doesn't seem to case though :(

For me using the repro project, when I CTRL+C it, the dotnet stays for at a couple minutes at least (stopped waiting and I ended up just killing it). On my actual project, it causes 3 dotnet processes to stay open, not sure why. At least, it seems like those processes are not locking down the DLLs anymore, so I can re-run dotnet run without having to kill the dotnet processes first.