Azure / azure-functions-durable-extension

Durable Task Framework extension for Azure Functions
MIT License
711 stars 263 forks source link

ActivityTrigger with byte[] as input results in null reference exception after upgrade to Microsoft.Azure.Functions.Worker.Extensions.DurableTask 1.1.1 #2746

Open Ruud2000 opened 4 months ago

Ruud2000 commented 4 months ago

Description

After upgrading from Microsoft.Azure.Functions.Worker.Extensions.DurableTask 1.0.4 to 1.1.1 my ActivityTrigger functions throw an exception on a byte[] binding parameter.

I have an Azure Durable Function and my orchestrator invokes an ActivityTrigger Function which returns a byte[][]. I then use a fan-out/fan-in pattern to process each byte[] in this array in parallel. With package version 1.0.4 this works, but after upgrading to 1.1.1 calling the ActivityTrigger throws an exception.

I reported this before in https://github.com/Azure/azure-functions-durable-extension/issues/2678 which was closed as duplicate and I hoped it would be resolved with version 1.1.1, but unfortunately the issue still occurs in the latest version.

Expected behavior

The Function with ActivityTrigger should accept byte[] as valid input.

Actual behavior

The following exception is thrown in the autogenerated DirectFunctionExecutor

System.NullReferenceException
  HResult=0x80004003
  Message=Object reference not set to an instance of an object.
  Source=FunctionApp
  StackTrace:
   at FunctionApp.DirectFunctionExecutor.<ExecuteAsync>d__3.MoveNext() in C:\Users\[redacted]\source\repos\Sandbox\FunctionApp\Microsoft.Azure.Functions.Worker.Sdk.Generators\Microsoft.Azure.Functions.Worker.Sdk.Generators.FunctionExecutorGenerator\GeneratedFunctionExecutor.g.cs:line 50

  This exception was originally thrown at this call stack:
    FunctionApp.DirectFunctionExecutor.ExecuteAsync(Microsoft.Azure.Functions.Worker.FunctionContext) in GeneratedFunctionExecutor.g.cs

Relevant source code snippets

Create a new empty Function App. Change the Program.cs to the following:

using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(services =>
    {
        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();
    })
    .Build();

host.Run();

And create class FanOutByteArrayOrchestrator.cs with the below content:

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using System.Text;
using System.Text.Json;

namespace FunctionApp
{
    public class FanOutByteArrayOrchestrator
    {
        [Function("FanOutByteArrayOrchestratorApi")]
        public async Task<HttpResponseData> RunApi(
            [HttpTrigger] HttpRequestData request,
            [DurableClient] DurableTaskClient durableTaskClient)
        {
            var orchestrationInstanceId = await durableTaskClient
                .ScheduleNewOrchestrationInstanceAsync("FanOutByteArrayOrchestrator")
                .ConfigureAwait(true);

            return durableTaskClient.CreateCheckStatusResponse(request, orchestrationInstanceId);
        }

        [Function("FanOutByteArrayOrchestrator")]
        public async Task Run(
            [OrchestrationTrigger] TaskOrchestrationContext taskOrchestrationContext)
        {
            var events = await taskOrchestrationContext
                .CallActivityAsync<byte[][]>("CreateEvents")
                .ConfigureAwait(true);

            // Fan-out/fan-in
            var eventTasks = events
                .Select(x => taskOrchestrationContext.CallActivityAsync("ProcessEvent", input: x))
                .ToList();
            await Task.WhenAll(eventTasks).ConfigureAwait(true);
        }

        [Function("CreateEvents")]
        public Task<byte[][]> CreateEvents([ActivityTrigger] CancellationToken cancellationToken)
        {
            var cars = new List<Car>
            {
                new() { HorsePower = 500, Name = "Tesla Model 3"},
                new() { HorsePower = 1000, Name = "Red Bull Formula 1 Car"},
            };

            var byteArray = cars.Select(x => Encoding.UTF8.GetBytes(JsonSerializer.Serialize(x)));

            return Task.FromResult(byteArray.ToArray());
        }

        [Function("ProcessEvent")]
        public Task ProcessEvent([ActivityTrigger] byte[] eventToProcess, CancellationToken cancellationToken)
        {
            var jsonString = Encoding.UTF8.GetString(eventToProcess);
            var car = JsonSerializer.Deserialize<Car>(jsonString);

            Console.WriteLine($"Name: {car?.Name} has {car?.HorsePower} horse power.");

            return Task.CompletedTask;
        }
    }

    internal class Car
    {
        public int HorsePower { get; set; }
        public string? Name { get; set; }
    }
}

Content of the .csproj file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <OutputType>Exe</OutputType>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.21.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.1.1" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.1.0" />
    <!--<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.2.1" />-->
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.17.0" />
    <PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="1.2.0" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
  <ItemGroup>
    <Using Include="System.Threading.ExecutionContext" Alias="ExecutionContext" />
  </ItemGroup>
</Project>

Screenshot

image

Known workarounds

Downgrade to Microsoft.Azure.Functions.Worker.Extensions.DurableTask 1.0.4.

If deployed to Azure

Same behavior is seen when deployed to Azure - in my case an isolated azure function app.