Dispose is not called in task CE
dustinmoris opened this issue · comments
Hi,
Using the latest version of Giraffe, which uses the latest version of TaskBuilder.fs
(the NuGet package) I run into an issue where the Dispose()
method is not being invoked from inside the task CE.
I have created the following project to reproduce the issue:
DisposableTest.fsproj:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<DebugType>portable</DebugType>
<AssemblyName>DisposableTest</AssemblyName>
<OutputType>Exe</OutputType>
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
<EnableDefaultContentItems>false</EnableDefaultContentItems>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.0.*" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="2.0.*" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.0.*" />
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="2.0.*"/>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.0.*" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.*" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.0.*" />
<PackageReference Include="Giraffe" Version="1.0.*" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>
</Project>
Program.fs:
module DisposableTest.App
open System
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.Extensions.Logging
open Microsoft.Extensions.DependencyInjection
open Giraffe
// ---------------------------------
// Types
// ---------------------------------
type DisposableObject (nr : int) =
let mutable disposed = false
let output msg = printfn "%s %i" msg nr
do output "CREATED"
let cleanup (disposing : bool) =
if not disposed then
if disposing then output "DISPOSING"
disposed <- true
output "DISPOSED"
else output "ALREADY DISPOSED"
interface IDisposable with
member this.Dispose() =
cleanup true
GC.SuppressFinalize this
override __.Finalize() =
cleanup(false)
// ---------------------------------
// Web app
// ---------------------------------
let testHandler : HttpHandler =
fun next ctx ->
task {
use x = new DisposableObject 1
return! text "Hi 1" next ctx
}
let testHandler2 : HttpHandler =
fun next ctx ->
use x = new DisposableObject 2
text "Hi 2" next ctx
let testHandler3 : HttpHandler =
fun next ctx ->
task {
let x = (new DisposableObject 3) :> IDisposable
try
return! text "Hi 3" next ctx
finally
x.Dispose()
}
let webApp =
choose [
route "/api" >=> testHandler
route "/api2" >=> testHandler2
route "/api3" >=> testHandler3
setStatusCode 404 >=> text "Not Found" ]
// ---------------------------------
// Error handler
// ---------------------------------
let errorHandler (ex : Exception) (logger : ILogger) =
logger.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.")
clearResponse >=> setStatusCode 500 >=> text ex.Message
// ---------------------------------
// Config and Main
// ---------------------------------
let configureApp (app : IApplicationBuilder) =
let env = app.ApplicationServices.GetService<IHostingEnvironment>()
(match env.IsDevelopment() with
| true -> app.UseDeveloperExceptionPage()
| false -> app.UseGiraffeErrorHandler errorHandler)
.UseGiraffe(webApp)
let configureServices (services : IServiceCollection) =
services.AddGiraffe() |> ignore
let configureLogging (builder : ILoggingBuilder) =
let filter (l : LogLevel) = l.Equals LogLevel.Error
builder.AddFilter(filter).AddConsole().AddDebug() |> ignore
[<EntryPoint>]
let main _ =
WebHostBuilder()
.UseKestrel()
.UseIISIntegration()
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
.Build()
.Run()
0
As you can see I have created a new type called DisposableObject
which implements IDisposable
according to best practice standards.
When I run the application and visit the following URLs
http://localhost:5000/api
http://localhost:5000/api2
http://localhost:5000/api3
... then I get the following output in the console:
Hosting environment: Production
Content root path: /Users/dustinmoris/Temp/DisposableTest/src/DisposableTest/bin/Debug/netcoreapp2.0/
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
CREATED 1
CREATED 2
DISPOSING 2
DISPOSED 2
CREATED 3
Only the second handler where there is no task {}
involved seems to properly dispose of the object.
Any ideas why? It is possible that Giraffe is doing something wrong, but we use the task CE normally from within ASP.NET Core without anything custom around it as far as I know.
The bug is in my ReturnFrom
implementation, and I think it got introduced when I was refactoring to try to support tail call optimization. If I'm correct, code that uses let! x = xTask; return x
should not be affected, vs return! xTask
which is affected. I'll fix it at lunchtime (couple hours from now).
Robert
This should be fixed in 1.0.1.
There is now also a test for this (combinations of TryFinally and ReturnFrom, both with and without exceptions in the wrapped continuation) to avoid future regressions.
Wow thank you for this quick fix! Much appreciated!