rspeele / TaskBuilder.fs

F# computation expression builder for System.Threading.Tasks

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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!