Azure / azure-functions-host

The host/runtime that powers Azure Functions
https://functions.azure.com
MIT License
1.95k stars 442 forks source link

Multiple awaits kill DI Scope #6029

Closed Spaceman1861 closed 4 years ago

Spaceman1861 commented 4 years ago

Hi all,

I have been having some issues with a Dryloc Exception(detail below). The exact same application in a console app works correctly.

I have posted this issue on the Mediatr GitHub think the issue may lay there but the creator is of the opinion that it is not. https://github.com/jbogard/MediatR/issues/516#event-3327307606

Investigative information

Happens every time in local debug or deployed

Repro steps

Call await Mediatr.Send() twice

Expected behavior

No exception

Actual behavior

Exception

  HResult=0x80131509
  Message=Scope disposed{no name} is disposed and scoped instances are disposed and no longer available.
  Source=Microsoft.Azure.WebJobs.Script.WebHost
  StackTrace:
   at DryIoc.Throw.It(Int32 error, Object arg0, Object arg1, Object arg2, Object arg3) in d:\a\1\s\src\WebJobs.Script.WebHost\DependencyInjection\DryIoc\Container.cs:line 8990
   at DryIoc.Scope.TryGet(Object& item, Int32 id) in d:\a\1\s\src\WebJobs.Script.WebHost\DependencyInjection\DryIoc\Container.cs:line 7880
   at DryIoc.Container.InstanceFactory.GetAndUnwrapOrDefault(IScope scope, Int32 factoryId) in d:\a\1\s\src\WebJobs.Script.WebHost\DependencyInjection\DryIoc\Container.cs:line 1479
   at DryIoc.Container.InstanceFactory.GetInstanceFromScopeChainOrSingletons(IResolverContext r) in d:\a\1\s\src\WebJobs.Script.WebHost\DependencyInjection\DryIoc\Container.cs:line 1468
   at DryIoc.Container.DryIoc.IResolver.Resolve(Type serviceType, IfUnresolved ifUnresolved) in d:\a\1\s\src\WebJobs.Script.WebHost\DependencyInjection\DryIoc\Container.cs:line 194
   at DryIoc.Resolver.Resolve[TService](IResolver resolver, IfUnresolved ifUnresolved) in d:\a\1\s\src\WebJobs.Script.WebHost\DependencyInjection\DryIoc\Container.cs:line 4741
   at DryIoc.Microsoft.DependencyInjection.DryIocAdapter.<>c__DisplayClass3_0.<RegisterDescriptor>b__0(IResolverContext r) in d:\a\1\s\src\WebJobs.Script.WebHost\DependencyInjection\DryIoc\DryIocAdapter.cs:line 156
   at DryIoc.Registrator.<>c__DisplayClass27_0.<RegisterDelegate>b__0(IResolverContext r) in d:\a\1\s\src\WebJobs.Script.WebHost\DependencyInjection\DryIoc\Container.cs:line 4540
   at DryIoc.Container.DryIoc.IResolver.Resolve(Type serviceType, Object serviceKey, IfUnresolved ifUnresolved, Type requiredServiceType, Request preResolveParent, Object[] args) in d:\a\1\s\src\WebJobs.Script.WebHost\DependencyInjection\DryIoc\Container.cs:line 307
   at DryIoc.Container.ResolveAndCacheDefaultFactoryDelegate(Type serviceType, IfUnresolved ifUnresolved) in d:\a\1\s\src\WebJobs.Script.WebHost\DependencyInjection\DryIoc\Container.cs:line 223
   at DryIoc.Container.DryIoc.IResolver.Resolve(Type serviceType, IfUnresolved ifUnresolved) in d:\a\1\s\src\WebJobs.Script.WebHost\DependencyInjection\DryIoc\Container.cs:line 194
   at Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection.JobHostServiceProvider.GetService(Type serviceType, IfUnresolved ifUnresolved) in d:\a\1\s\src\WebJobs.Script.WebHost\DependencyInjection\JobHostServiceProvider.cs:line 99
   at Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection.JobHostServiceProvider.GetService(Type serviceType) in d:\a\1\s\src\WebJobs.Script.WebHost\DependencyInjection\JobHostServiceProvider.cs:line 77
   at MediatR.ServiceFactoryExtensions.GetInstances[T](ServiceFactory factory)
   at MediatR.Internal.RequestHandlerWrapperImpl`2.Handle(IRequest`1 request, CancellationToken cancellationToken, ServiceFactory serviceFactory)
   at MediatR.Mediator.Send[TResponse](IRequest`1 request, CancellationToken cancellationToken)

Known workarounds

Using .GetAwaiter().GetResult(); or .Wait();

Related information

Using these packages:

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="MediatR" Version="8.0.1" />
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.7" />
  </ItemGroup>

With this function:

public class ReleaseManagerTimerFunction
{
    private readonly IMediator _mediator;
    public ReleaseManagerTimerFunction(IMediator mediator) => _mediator = mediator;

    [FunctionName(nameof(ReleaseManagerTimerFunction))]
    public async void Run(
        //[TimerTrigger("0 20 * * *")]
        [TimerTrigger("* * * * *")]
        TimerInfo timerInfo
    )
    {
        await _mediator.Send(new ReleaseManagerServiceCommand());
    }
}

Which calls this handler

public class ReleaseManagerService : IRequestHandler<ReleaseManagerServiceCommand, ResponseModel<bool>>
{
    private readonly IMediator _mediator;

    public ReleaseManagerService(
        IMediator mediator
    )
    {
        _mediator = mediator;
    }

    public async Task<ResponseModel<bool>> Handle(ReleaseManagerServiceCommand request, CancellationToken cancellationToken)
    {
            // Using more then one mediator await breaks
            //var a = await _mediator.Send(new b(), cancellationToken);
            // modifying the code to this works
            var a = _mediator
                .Send(new b(), cancellationToken)
                .GetAwaiter()
                .GetResult();

            var b = await _mediator.Send(new c(), cancellationToken);
    }
}

Which in turn calls these (simplifed for debugging):

public class bHandler : IRequestHandler<b, ResponseModel<bool>>
{
    private readonly HttpClient _client;

    public bHandler(
        IHttpClientFactory clientFactory
    )
    {
        _client = clientFactory.CreateClient("b");
    }

    public async Task<ResponseModel<bool>> Handle(b command, CancellationToken cancellationToken)
    {
        await _client.PostAsync(Endpoint,
            new StringContent(
                JObject.FromObject(new {command.Body}).ToString(),
                Encoding.UTF8, "application/json"
            ),
            cancellationToken
        );

        return new ResponseModel<bool>(true);
    }
}

public class cHandler : IRequestHandler<c, ResponseModel<bool>>
{
    private readonly HttpClient _client;

    public cHandler(
        IHttpClientFactory clientFactory
    )
    {
        _client = clientFactory.CreateClient("c");
    }

    public async Task<ResponseModel<bool>> Handle(c command, CancellationToken cancellationToken)
    {
        await _client.PostAsync(Endpoint,
            new StringContent(
                JObject.FromObject(new {command.Body}).ToString(),
                Encoding.UTF8, "application/json"
            ),
            cancellationToken
        );

        return new ResponseModel<bool>(true);
    }
}
fabiocav commented 4 years ago

Thanks for opening this.

Couple of questions:

Spaceman1861 commented 4 years ago

I will review both your questions and get back to you.

ghost commented 4 years ago

This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 4 days. It will be closed if no further activity occurs within 3 days of this comment.

Spaceman1861 commented 4 years ago

Sorry for the long delay on this.

I have generated this repro that demonstrates that httpclientfactory alone is no the issue.

[assembly: FunctionsStartup(typeof(Startup))]
namespace _6029Repro
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddHttpClient();
        }
    }
}

namespace _6029Repro
{
    public class ReleaseManagerTimerFunctionNoMediator
    {
        private readonly IHttpClientFactory _httpClientFactory;

        public ReleaseManagerTimerFunctionNoMediator(IHttpClientFactory httpClientFactory) =>
            _httpClientFactory = httpClientFactory;

        [FunctionName(nameof(ReleaseManagerTimerFunctionNoMediator))]
        public async Task Run(
            //[TimerTrigger("0 20 * * *")]
            [TimerTrigger("* * * * *")] TimerInfo timerInfo
        )
        {
            HttpClient clientA;
            HttpClient clientB;

            const string clientAEndpoint = "https://api.nasa.gov/planetary/apod";
            const string clientBEndpoint = "https://ssd-api.jpl.nasa.gov/fireball.api";

            clientA = _httpClientFactory.CreateClient(nameof(clientA));
            clientB = _httpClientFactory.CreateClient(nameof(clientB));

            var clientAResponse = await clientA.GetAsync(clientAEndpoint);
            var clientBResponse = await clientB.GetAsync(clientBEndpoint);

            Console.WriteLine(await clientAResponse.Content.ReadAsStringAsync());
            Console.WriteLine(await clientBResponse.Content.ReadAsStringAsync());
        }
    }
}

That works fine.

Spaceman1861 commented 4 years ago

Oddly I cannot replicate this anymore.

I have updated dotnet runtime since to 3.1.5 so maybe that fixed it :|

This works for me now.

[assembly: FunctionsStartup(typeof(Startup))]
namespace _6029Repro
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddHttpClient();
            builder.Services.AddMediatR(typeof(Startup));
        }
    }

    public class ReleaseManagerTimerFunctionNoMediator
    {
        private readonly IMediator _mediator;

        public ReleaseManagerTimerFunctionNoMediator(IMediator mediator) =>
            _mediator = mediator;

        [FunctionName(nameof(ReleaseManagerTimerFunctionNoMediator))]
        public async Task Run(
            //[TimerTrigger("0 20 * * *")]
            [TimerTrigger("* * * * *")] TimerInfo timerInfo
        )
        {
            await _mediator.Send(new ParentRequest());
        }
    }

    public class ParentRequest : IRequest { }
    public class ClientARequest : IRequest { }
    public class ClientBRequest : IRequest { }

    public class ParentHandler : IRequestHandler<ParentRequest>
    {
        private readonly IMediator _mediator;

        public ParentHandler(IMediator mediator) =>
            _mediator = mediator;

        public async Task<Unit> Handle(ParentRequest request, CancellationToken cancellationToken = default)
        {
            await _mediator.Send(new ClientARequest(), cancellationToken);
            await _mediator.Send(new ClientBRequest(), cancellationToken);

            return Unit.Value;
        }
    }

    public class ClientARequestHandler : IRequestHandler<ClientARequest>
    {
        private readonly IHttpClientFactory _httpClientFactory;

        public ClientARequestHandler(IHttpClientFactory httpClientFactory) =>
            _httpClientFactory = httpClientFactory;

        public async Task<Unit> Handle(ClientARequest request, CancellationToken cancellationToken = default)
        {
            HttpClient clientA;

            const string clientAEndpoint = "https://api.nasa.gov/planetary/apod";

            clientA = _httpClientFactory.CreateClient(nameof(clientA));

            var clientAResponse = await clientA.GetAsync(clientAEndpoint, cancellationToken);

            Console.WriteLine(await clientAResponse.Content.ReadAsStringAsync());

            return Unit.Value;
        }
    }

    public class ClientBRequestHandler : IRequestHandler<ClientBRequest>
    {
        private readonly IHttpClientFactory _httpClientFactory;

        public ClientBRequestHandler(IHttpClientFactory httpClientFactory) =>
            _httpClientFactory = httpClientFactory;

        public async Task<Unit> Handle(ClientBRequest request, CancellationToken cancellationToken = default)
        {
            HttpClient clientB;

            const string clientBEndpoint = "https://ssd-api.jpl.nasa.gov/fireball.api";

            clientB = _httpClientFactory.CreateClient(nameof(clientB));

            var clientBResponse = await clientB.GetAsync(clientBEndpoint, cancellationToken);

            Console.WriteLine(await clientBResponse.Content.ReadAsStringAsync());

            return Unit.Value;
        }
    }
}

Im going to close for now