Skip to content

NullReferenceException in OrchestrationEntityContext.RecoverLockAfterCall #3186

@mllab-nl

Description

@mllab-nl

Description

I observe null reference in /_/src/DurableTask.Core/Entities/OrchestrationEntityContext.cs:line 193

As it looks like things are not going as expected.
Could you please check if it is not a result of "Incorrect usage"

Simplified use case:
We need to generate reports.
To speed up, scale out and reuse(cache) the report is broken into logically independent pieces
Each peace is calculated separately as a separate activity and its results are cached into Stateful entity.
Before calculating specific piece the corresponding entity is locked to avoid work duplication.

Thus the orchestration creates a lot of tasks.
Each task reads the cached entity and if it is not calculated yet, locks it, calls the heavy activity, updates the cached entity with the result.
The orchestration combines the result (lightweight operation, last part of the orchestation)

Often, when some of the pieces are not yet in the cache the orchestration will fail with the Null Reference in the Core.

  1. Would you suggest to implement the use case in this way?
  2. Do we use it correctly?

Expected behavior

It works

Actual behavior

Null Reference Exception in the Core framework.

Relevant source code snippets

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Entities;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;

namespace ReportBuilder.Orchestations
{
    public static class EntityLockNullReferenceOrchestation
    {
        [Function(nameof(EntityLockNullReferenceOrchestation))]
        public static async Task<string> RunOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context, string[] tags)
        {
            ILogger logger = context.CreateReplaySafeLogger(nameof(EntityLockNullReferenceOrchestation));
            var orchestrationId = context.InstanceId;

            var tagsData = new ConcurrentQueue<Tuple<string, string>>();
            var categoriesReportsTasks = tags.Select(tag => GetOrBuildTagData(context, tag, tagsData)).ToArray();
            await Task.WhenAll(categoriesReportsTasks);
            return string.Join(", ", tagsData.Select(t => $"{t.Item1}: {t.Item2}"));
        }
        private static async Task GetOrBuildTagData(TaskOrchestrationContext context, string tag, ConcurrentQueue<Tuple<string, string>> tagsData)
        {
            var dataSet1Id = new EntityInstanceId(nameof(DummyTagCacheEntity), tag);
            var tagResult = await context.Entities.CallEntityAsync<string>(dataSet1Id, nameof(DummyTagCacheEntity.Get));
            if (string.IsNullOrEmpty(tagResult))
            {
                await using (await context.Entities.LockEntitiesAsync(dataSet1Id))
                {
                    tagResult = await context.CallActivityAsync<string>(nameof(DummyTagReport), tag);
                    await context.Entities.CallEntityAsync(dataSet1Id, nameof(DummyTagCacheEntity.Set), tagResult);
                }
            }
            tagsData.Enqueue(new(tag, tagResult));
        }
        
        [Function(nameof(DummyTagReport))]
        public static async Task<string> DummyTagReport([ActivityTrigger] string tag, FunctionContext executionContext)
        {
            ILogger logger = executionContext.GetLogger(nameof(DummyTagReport));
            logger.LogInformation("Generating report for tag '{tag}'", tag);

            // Simulate async call sending notification
            await Task.Delay(TimeSpan.FromSeconds(5));

            return $"{tag}.Result";
        }


        [Function($"{nameof(EntityLockNullReferenceOrchestation)}{nameof(HttpStart)}")]
        public static async Task<HttpResponseData> HttpStart(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
            [DurableClient] DurableTaskClient client,
            FunctionContext executionContext)
        {
            var tags = req.Query["tags"] ?? "tag1,tag2,";
            var tagsArray = tags.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(tag => tag.Trim()).ToArray();

            
            ILogger logger = executionContext.GetLogger(nameof(EntityLockNullReferenceOrchestation) + nameof(HttpStart));

            string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(EntityLockNullReferenceOrchestation), tagsArray);
            logger.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId);
            return await client.CreateCheckStatusResponseAsync(req, instanceId);
        }

    }
    public class DummyTagCacheEntity: TaskEntity<string>
    {
        public string Get() => State;

        public void Set(string value) => State = value;
        protected override string InitializeState(TaskEntityOperation entityOperation)
        {
            return string.Empty;
        }
        [Function(nameof(DummyTagCacheEntity))]
        public Task RunEntityAsync([EntityTrigger] TaskEntityDispatcher dispatcher)
        {
            return dispatcher.DispatchAsync(this);
        }
    }
}

Known workarounds

Provide a description of any known workarounds you used.

App Details

Local
All latest versions

<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.23.0" />
	<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
	<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Timer" Version="4.3.1" />
	<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.7.1" />
	<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.0.0" />
	<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.5" />
	<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
	<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.0.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.0.2" />

Screenshots

If applicable, add screenshots to help explain your problem.

Image

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions