只有在持久功能中扇出(并忘记)

时间:2018-05-17 13:36:15

标签: c# azure azure-functions serverless azure-durable-functions

我有一个带有2个功能和存储队列的现有功能应用程序。 F1由服务总线主题中的消息触发。对于接收到的每个消息,F1计算一些子任务(T1,T2,...),这些子任务必须以不同的延迟量执行。 Ex - T1将在3分钟后触发,T2在5分钟后触发等.F1将消息发布到具有适当可见性超时的存储队列(以模拟延迟),并且只要队列中显示消息就会触发F2。一切正常。

我现在想要迁移此应用以使用“耐用功能”#39;。 F1现在只启动协调器。 Orchestrator代码如下 -

    public static async Task Orchestrator([OrchestrationTrigger] DurableOrchestrationContext context, TraceWriter log)
    {
        var results = await context.CallActivityAsync<List<TaskInfo>>("CalculateTasks", "someinput");
        List<Task> tasks = new List<Task>();
        foreach (var value in results)
        {
            var pnTask = context.CallActivityAsync("PerformSubTask", value);
            tasks.Add(pnTask);
        }

        //dont't await as we want to fire and forget. No fan-in!
        //await Task.WhenAll(tasks);
    }

    [FunctionName("PerformSubTask")]
    public async static Task Run([ActivityTrigger]TaskInfo info, TraceWriter log)
    {
         TimeSpan timeDifference = DateTime.UtcNow - info.Origin.ToUniversalTime();
         TimeSpan delay = TimeSpan.FromSeconds(info.DelayInSeconds);
         var actualDelay = timeDifference > delay ? TimeSpan.Zero : delay - timeDifference;

         //will still keep the activity function running and incur costs??
         await Task.Delay(actualDelay);

         //perform subtask work after delay! 
    }

我只想扇出(没有粉丝收集结果)并启动子任务。协调器启动所有任务并避免调用等待Task.WhenAll&#39;。活动功能调用&#39; Task.Delay&#39;等待指定的时间,然后继续工作。

我的问题

  • 在此工作流程中使用持久性功能是否有意义?
  • 这是正确的方法来协调&#39;扇出&#39;工作流?
  • 我不喜欢活动功能在指定的时间内运行(3或5分钟)无所事事。它会产生成本,还是?
  • 此外,如果延迟超过10分钟,活动功能no way可以通过此方法取得成功!
  • 我之前尝试避免这种情况的方法是使用&#39; CreateTimer&#39;在orchestrator中然后添加活动作为延续,但我在“历史记录”中只看到了计时器条目。表。继续不开火!我是否违反constraint for orchestrator code - &#39; Orchestrator代码必须永远不会启动任何异步操作&#39; ?

    foreach (var value in results)
    {
            //calculate time to start
            var timeToStart = ;
            var pnTask = context.CreateTimer(timeToStart , CancellationToken.None).ContinueWith(t => context.CallActivityAsync("PerformSubTask", value));
            tasks.Add(pnTask);
    }
    

    更新:使用Chris建议的方法

    计算子任务和延迟的活动

    [FunctionName("CalculateTasks")]
    public static List<TaskInfo> CalculateTasks([ActivityTrigger]string input,TraceWriter log)
    {
        //in reality time is obtained by calling an endpoint 
        DateTime currentTime = DateTime.UtcNow;
        return new List<TaskInfo> {
            new TaskInfo{ DelayInSeconds = 10, Origin = currentTime },
            new TaskInfo{ DelayInSeconds = 20, Origin = currentTime },
            new TaskInfo{ DelayInSeconds = 30, Origin = currentTime },
        };
    }
    
    public static async Task Orchestrator([OrchestrationTrigger] DurableOrchestrationContext context, TraceWriter log)
    {
        var results = await context.CallActivityAsync<List<TaskInfo>>("CalculateTasks", "someinput");
        var currentTime = context.CurrentUtcDateTime;
        List<Task> tasks = new List<Task>();
        foreach (var value in results)
        {
            TimeSpan timeDifference = currentTime - value.Origin;
            TimeSpan delay = TimeSpan.FromSeconds(value.DelayInSeconds);
            var actualDelay = timeDifference > delay ? TimeSpan.Zero : delay - timeDifference;
    
            var timeToStart = currentTime.Add(actualDelay);
    
            Task delayedActivityCall = context
                 .CreateTimer(timeToStart, CancellationToken.None)
                 .ContinueWith(t => context.CallActivityAsync("PerformSubtask", value));
            tasks.Add(delayedActivityCall);
        }
    
        await Task.WhenAll(tasks);
    }
    

在协调器中简单地安排任务似乎有效。在我的情况下,我在循环之前计算任务和另一个活动(CalculateTasks)中的延迟。我希望使用&#39;当前时间&#39;来计算延迟。当活动运行时。我在活动中使用DateTime.UtcNow。在协调器中使用时,这种方式不能很好地发挥作用。由&#39; ContinueWith&#39;指定的活动只是不要跑步,而协调者总是在跑步&#39;州。

我可以不在协调器中使用活动记录的时间吗?

更新2

所以Chris建议的解决方法有效!

由于我不想收集活动的结果,因此我避免致电&#39; await Tasks.WhenAll(tasks)&#39;在安排所有活动之后。我这样做是为了减少控制队列上的争用,即如果需要则能够启动另一个业务流程。尽管如此,“协调者”的地位仍然存在。仍在&#39; 正在运行&#39;直到所有活动结束。我想它会转移到&#39; 完成&#39;只有在最后一个活动发布完成后才会发布。消息到控制队列。

我是对的吗?有没有办法在调度所有活动之后立即释放协调器?

3 个答案:

答案 0 :(得分:3)

ContinueWith方法对我来说很好。我能够使用以下orchestrator代码模拟您的场景的一个版本:

[FunctionName("Orchestrator")]
public static async Task Orchestrator(
    [OrchestrationTrigger] DurableOrchestrationContext context,
    TraceWriter log)
{
    var tasks = new List<Task>(10);
    for (int i = 0; i < 10; i++)
    {
        int j = i;
        DateTime timeToStart = context.CurrentUtcDateTime.AddSeconds(10 * j);
        Task delayedActivityCall = context
            .CreateTimer(timeToStart, CancellationToken.None)
            .ContinueWith(t => context.CallActivityAsync("PerformSubtask", j));
        tasks.Add(delayedActivityCall);
    }

    await Task.WhenAll(tasks);
}

对于它的价值,这里是活动功能代码。

[FunctionName("PerformSubtask")]
public static void Activity([ActivityTrigger] int j, TraceWriter log)
{
    log.Warning($"{DateTime.Now:o}: {j:00}");
}

从日志输出中,我看到所有活动调用彼此间隔10秒。

另一种方法是扇出多个子编排(如@jeffhollan建议),这是一个简单的持续计时器延迟和活动调用的短序列。

<强>更新 我尝试使用您更新的样本,并能够重现您的问题!如果在Visual Studio中本地运行并将异常设置配置为始终中断异常,则应该看到以下内容:

  

System.InvalidOperationException :'检测到多线程执行。如果协调器功能代码等待不是由DurableOrchestrationContext方法创建的任务,则会发生这种情况。更多细节可以在本文https://docs.microsoft.com/en-us/azure/azure-functions/durable-functions-checkpointing-and-replay#orchestrator-code-constraints中找到。'

这意味着调用context.CallActivityAsync("PerformSubtask", j)的线程与调用orchestrator函数的线程相同。我不知道为什么我的初始例子没有达到这一点,或者为什么你的版本没有。它与TPL决定使用哪个线程来运行ContinueWith委托有关 - 这是我需要进一步研究的内容。

好消息是有一个简单的解决方法,即指定TaskContinuationOptions.ExecuteSynchronously,如下所示:

Task delayedActivityCall = context
    .CreateTimer(timeToStart, CancellationToken.None)
    .ContinueWith(
        t => context.CallActivityAsync("PerformSubtask", j),
        TaskContinuationOptions.ExecuteSynchronously);

请尝试并告诉我是否能解决您正在观察的问题。

理想情况下,在使用Task.ContinueWith时,您无需执行此解决方法。我在GitHub中打开了一个问题来跟踪这个问题:https://github.com/Azure/azure-functions-durable-extension/issues/317

  

由于我不想收集活动的结果,所以我在安排所有活动后避免调用await Tasks.WhenAll(tasks)。我这样做是为了减少控制队列上的争用,即如果需要则能够启动另一个业务流程。然而,“协调者”的状态仍然是“正在运行”,直到所有活动完成。我想只有在最后一个活动将“完成”消息发布到控制队列后才会转到“完成”。

这是预期的。在完成所有未完成的持久性任务之前,Orchestrator功能永远不会完成。没有办法解决这个问题。请注意,您仍然可以启动其他orchestrator实例,如果它们碰巧落在同一个分区上,可能会有一些争用(默认情况下有4个分区)。

答案 1 :(得分:2)

await Task.Delay绝对不是最好的选择:你将支付这段时间,而你的功能不会做任何有用的事情。消费计划的最大延迟也约为10分钟。

在我看来,原始队列消息是“即发即弃”场景的最佳选择。设置适当的可见性超时,您的方案将可靠而有效地工作。

持久功能的杀手功能是await s,它们在保持范围的同时,可以暂停和恢复它们。因此,这是实现扇入的好方法,但你不需要它。

答案 2 :(得分:0)

我认为耐用性对于这个工作流程肯定有意义。我认为最好的选择是如你所说的那样利用延迟/定时器功能,但基于执行的同步性,我不认为我会将所有内容添加到任务列表中,这实际上是期望{{1或者你没有瞄准的.WhenAll()。我想我个人只会为每个任务执行一个带有计时器延迟的连续foreach循环。所以伪代码:

.WhenAny()

你需要那些等待的人,所以只是避免 for(int x = 0; x < results.Length; x++) { await context.CreateTimer(TimeSpan.FromMinutes(1), ...); await context.CallActivityAsync("PerformTaskAsync", results[x]); } 可能会导致上面的代码示例中出现一些问题。希望有所帮助