本地运行Azure Durable Functions子编排时WaitForExternalEvent任务创建失败
我在测试.NET 8隔离模式下的C# Azure Durable Functions,工作流如下:
- 通过HTTP触发器启动带数据的新编排
- 将数据存储到Blob存储
- 调用子编排
- 子编排执行文件下载操作
- 子编排调用WaitForExternalEvent等待外部事件
- 外部事件通过HTTP触发器接收后,子编排完成并返回
本地使用VS Code + Azurite存储模拟器运行,在子编排中创建WaitForExternalEvent任务(甚至还没await):
var finished = context.WaitForExternalEvent<string>("Finished");
就会抛出异常:
检测到无效的异步调用。这可能是由于在编排器函数实现中等待非持久任务,或者是调用异步代码的中间件导致的。
但相同代码在主编排中运行正常,不管代码是否在同一个类中都会出现这个问题,想问是不是遗漏了编排/子编排和WaitForExternalEvent相关的配置,或是VS Code设置的问题?
简化代码示例
主编排函数/类
public static class MainOrchestration { [Function(nameof(MainOrchestration))] public static async Task<List<string>> RunOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context, OrchestrationInputDto orchestrationInput) { ILogger logger = context.CreateReplaySafeLogger(nameof(MainOrchestration)); List<JobDetails> jobDetails = await RouteDocument(logger, context, orchestrationInput); foreach (JobDetails jobDetail in jobDetails) logger.LogInformation("Do some logging"); var outputs = new List<string>(); return outputs; } public static async Task<List<JobDetails>> RouteDocument(ILogger logger, TaskOrchestrationContext context, OrchestrationInputDto orchestrationInput) { var parallelTasks = new List<Task<JobDetails>>(); context.SetCustomStatus("Document is being processed"); Task<JobDetails> task = context.CallSubOrchestratorAsync<JobDetails>(nameof(SubOrchestration), orchestrationInput); parallelTasks.Add(task); await Task.WhenAll(parallelTasks); List<JobDetails> result = []; foreach (Task<JobDetails> subOrchestration in parallelTasks) { result.Add(await subOrchestration); } return result; } [Function("ReceiveFileWithParameters")] public static async Task<HttpResponseData> HttpStart( [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req, [DurableClient] DurableTaskClient client, FunctionContext executionContext) { // 解析输入、存储文件并启动主编排实例 ... string instanceId = await client.ScheduleNewOrchestrationInstanceAsync( nameof(MainOrchestration), new OrchestrationInputDto(requestParametersDto, storeFileParameters), null); logger.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId); var response = client.CreateCheckStatusResponse(req, instanceId); return response; } }
子编排函数/类
public static class SubOrchestration { [Function(nameof(SubOrchestration))] public static async Task<JobDetails> RunOrchestrator( [OrchestrationTrigger] TaskOrchestrationContext context, OrchestrationInputDto orchestrationInput) { ILogger logger = context.CreateReplaySafeLogger(nameof(SubOrchestration)); JobDetails jobDetail = await SendFileToFunction(orchestrationInput); // 等待外部事件 try { var finished = context.WaitForExternalEvent<string>("Finished"); var failed = context.WaitForExternalEvent<string>("Failed"); var winner = await Task.WhenAny(finished, failed); if (winner == finished) { logger.LogInformation("Successful"); } else if (winner == failed) { logger.LogInformation("Failed"); } } catch (Exception) { logger.LogInformation("Error"); } return jobDetail; } [Function("SendFileToFunction")] public static async Task<JobDetails> SendFileToFunction([ActivityTrigger] OrchestrationInputDto orchestrationInput) { /// 暂时只返回一个JobDetails实体 return new JobDetails(...); } [Function("ReceiveExternalEvent")] public static async Task ReceiveExternalEvent( [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req, [DurableClient] DurableTaskClient client, FunctionContext executionContext) { ILogger logger = executionContext.GetLogger(nameof(ReceiveExternalEvent)); logger.LogInformation("ReceiveExternalEvent HTTP trigger function processed a request."); await client.RaiseEventAsync(instanceId, "Finished", "true"); } }
问题根源与解决方法
根源分析
异常的核心原因是子编排中直接调用了活动函数SendFileToFunction,而没有通过Durable的上下文方法发起调用。Durable编排器要求所有异步操作必须通过TaskOrchestrationContext提供的方法(如CallActivityAsync、CallSubOrchestratorAsync、WaitForExternalEvent等)执行,否则会被框架判定为非持久化异步操作,触发"无效异步调用"异常。
解决步骤
- 修改活动函数调用方式:将子编排中直接调用活动函数的代码,替换为通过
context.CallActivityAsync发起调用:
// 原错误代码 JobDetails jobDetail = await SendFileToFunction(orchestrationInput); // 修改后代码 JobDetails jobDetail = await context.CallActivityAsync<JobDetails>(nameof(SendFileToFunction), orchestrationInput);
验证外部事件目标实例ID:确保
ReceiveExternalEvent函数中调用RaiseEventAsync时,传入的是子编排的实例ID,而非主编排ID。可以在主编排调用子编排时记录子实例ID,或者通过Durable实例查询接口获取,避免事件无法被子编排接收。保持编排代码确定性:所有编排器(包括子编排)中的异步操作都必须使用Durable框架提供的方法,确保代码的可重放性和状态可跟踪性。
内容的提问来源于stack exchange,提问作者Jens

