我在文档中找不到有实质性的信息,因此我自己进行了一些测试,因为这个问题让我很感兴趣。我还包括了一个潜在的"解决方案"来解决您的多步问题,或者说,它还没有幂等(但是吗?)。请浏览整篇文字,它在最后。
首先,如果第一次调用超过默认的30秒响应时间限制,只要两个实例运行相同的事件,那么两个实例都可以运行同一个事件,这就是为什么提到“至少一次”传递的原因。例如:
- 事件已发布
- 实例A接收事件
- 实例A运行超过30秒
- 将事件添加到“重试队列”,因为实例A尚未回复
- Instance A完成,总共耗时35秒
- 从重试队列中发布超过10秒的事件
- 实例B接收事件
我真的认为,两个实例的执行持续时间会影响EventGrid的反应。
根据Retry schedule文档:
如果端点在3分钟内响应,则Event Grid将尽最大努力从重试队列中删除事件,但仍可能收到重复事件。
这可以解释为,只要一个实例成功响应,EventGrid将会尝试清除重试队列(从上面的示例来看,实例B不应该再接收到该事件)。但是,我也想知道当实例A失败时会发生什么。
我使用以下代码进行了尝试,在一定延迟后,实例A将抛出异常:
public static class Function1
{
private static Dictionary<string, int> _Counter = new();
[FunctionName("Function1")]
public static async Task Run([EventGridTrigger] EventGridEvent eventGridEvent, ILogger log)
{
if (_Counter.ContainsKey(eventGridEvent.Id))
{
_Counter[eventGridEvent.Id]++;
log.LogInformation($"{eventGridEvent.Id}: {_Counter[eventGridEvent.Id]}");
return;
}
_Counter.Add(eventGridEvent.Id, 1);
var delay = (long)eventGridEvent.Data;
log.LogInformation($"Delay: {delay}");
await Task.Delay(TimeSpan.FromSeconds(delay));
log.LogInformation($"{eventGridEvent.Id}: {_Counter[eventGridEvent.Id]}");
throw new Exception($"{eventGridEvent.Id} Throwing after {delay} seconds delay");
}
}
不同延迟的结果
延迟 = 35
2022-02-06T00:34:10.798 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:34:10.7899831+00:00', Id=9d1adfe9-21bd-454c-862e-a4d8d5b312e0)
2022-02-06T00:34:10.801 [Information] Delay: 35
2022-02-06T00:34:45.807 [Information] 6d3d141d-d0ee-4e81-a648-e2497e562b84: 1
2022-02-06T00:34:45.894 [Error] Executed 'Function1' (Failed, Id=9d1adfe9-21bd-454c-862e-a4d8d5b312e0, Duration=35096ms)6d3d141d-d0ee-4e81-a648-e2497e562b84 Throwing after 35 seconds delay
2022-02-06T00:34:55.949 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:34:55.9486015+00:00', Id=58a7a6db-2f97-4bc7-b878-030b8be25711)
2022-02-06T00:34:55.949 [Information] 6d3d141d-d0ee-4e81-a648-e2497e562b84: 2
2022-02-06T00:34:55.949 [Information] Executed 'Function1' (Succeeded, Id=58a7a6db-2f97-4bc7-b878-030b8be25711, Duration=1ms)
看起来,在30秒后最有可能被添加到队列中的重试要么被移除/替换了,要么它的预定执行时间被"更新":
- 58-13 = 45,
- 实例A执行持续时间为35秒
- 实例A失败后重试延迟10秒
延迟时间 = 45秒
2022-02-06T00:37:29.526 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:37:29.5262414+00:00', Id=c4596efa-7034-4efe-bcf9-85d502517711)
2022-02-06T00:37:29.526 [Information] Delay: 45
2022-02-06T00:38:14.524 [Information] 284fa140-1cdc-41e7-973f-d0ecd6f19692: 1
2022-02-06T00:38:14.530 [Error] Executed 'Function1' (Failed, Id=c4596efa-7034-4efe-bcf9-85d502517711, Duration=44999ms)284fa140-1cdc-41e7-973f-d0ecd6f19692 Throwing after 45 seconds delay
2022-02-06T00:38:24.590 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:38:24.5901035+00:00', Id=43a1fafa-7773-44e5-9f3a-b4bf697865a9)
2022-02-06T00:38:24.590 [Information] 284fa140-1cdc-41e7-973f-d0ecd6f19692: 2
2022-02-06T00:38:24.590 [Information] Executed 'Function1' (Succeeded, Id=43a1fafa-7773-44e5-9f3a-b4bf697865a9, Duration=0ms)
这是一种意外的行为。我们本应该预期在第一个调用开始后40秒内,看到第二个调用完成,而且很可能会在第一个调用之后完成。这可能表明EventGrid实际上知道第一个实例尚未失败,仍在运行,因此它实际上还没有将重试尝试添加到队列中。然而,它在第一次实例失败后添加了它,而且如预期的那样,在10秒后执行了重试尝试。
我想测试上述内容,并且认为魔数可能是3分钟(文档中也提到),而不是30秒。
延迟=185
2022-02-06T00:40:20.381 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:40:20.3809687+00:00', Id=454b815f-6ca6-4b75-9272-711c7b6bf42a)
2022-02-06T00:40:20.381 [Information] Delay: 185
2022-02-06T00:43:00.393 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:43:00.3928958+00:00', Id=23910142-d903-4b9b-bec2-a5665e8e1db7)
2022-02-06T00:43:00.393 [Information] 97d828e6-d1f3-4523-b043-1f76465de6ea: 2
2022-02-06T00:43:00.393 [Information] Executed 'Function1' (Succeeded, Id=23910142-d903-4b9b-bec2-a5665e8e1db7, Duration=0ms)
2022-02-06T00:43:25.381 [Information] 97d828e6-d1f3-4523-b043-1f76465de6ea: 2
2022-02-06T00:43:25.387 [Error] Executed 'Function1' (Failed, Id=454b815f-6ca6-4b75-9272-711c7b6bf42a, Duration=185002ms)97d828e6-d1f3-4523-b043-1f76465de6ea Throwing after 185 seconds delay
这也有点违背文档说明。第一个实例在40:20执行,43:00(本应预期为43:20),运行第二个实例并立即完成。然后第一个实例在43:25失败(如预期)。也许是3分钟而不是30秒。但是EventGrid仅查看时间戳的分钟部分。我们可以从中得出的一件事是,第一个实例的延迟失败不会触发第三次调用。
让我们改变异常抛出的时间;第二次调用将会抛出一个异常,像这样:
public static class Function1
{
private static Dictionary<string, int> _Counter = new();
[FunctionName("Function1")]
public static async Task Run([EventGridTrigger] EventGridEvent eventGridEvent, ILogger log)
{
if (_Counter.ContainsKey(eventGridEvent.Id))
{
_Counter[eventGridEvent.Id]++;
throw new Exception($"{eventGridEvent.Id}: {_Counter[eventGridEvent.Id]}");
}
_Counter.Add(eventGridEvent.Id, 1);
var delay = (long)eventGridEvent.Data;
log.LogInformation($"Delay: {delay}");
await Task.Delay(TimeSpan.FromSeconds(delay));
log.LogInformation($"{eventGridEvent.Id} finished after {delay}: {_Counter[eventGridEvent.Id]}");
return;
}
}
延迟时间=35
2022-02-06T00:57:00.777 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:57:00.7684489+00:00', Id=16505ac4-0cb2-4b1a-8b7c-d4adf964fe14)
2022-02-06T00:57:00.780 [Information] Delay: 35
2022-02-06T00:57:35.794 [Information] 7fce3bc0-f5d3-4b3e-bd5d-4997299aa9b7 finished after 35: 1
2022-02-06T00:57:35.796 [Information] Executed 'Function1' (Succeeded, Id=16505ac4-0cb2-4b1a-8b7c-d4adf964fe14, Duration=35028ms)
30秒后仍然没有任何结果。跳过45,因为我非常确定它会产生与35相同的结果。
延迟=185
2022-02-06T00:59:04.226 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:59:04.2262900+00:00', Id=cac343e5-911b-4ba7-a0c7-69e883a61392)
2022-02-06T00:59:04.227 [Information] Delay: 185
2022-02-06T01:01:44.196 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T01:01:44.1962398+00:00', Id=2c65d66b-57c8-4a7d-b9ec-d54959e84e3d)
2022-02-06T01:01:44.271 [Error] Executed 'Function1' (Failed, Id=2c65d66b-57c8-4a7d-b9ec-d54959e84e3d, Duration=68ms)e051565c-9900-4cef-9786-5795c9fc7f91: 2
2022-02-06T01:02:09.240 [Information] e051565c-9900-4cef-9786-5795c9fc7f91 finished after 185: 2
2022-02-06T01:02:09.240 [Information] Executed 'Function1' (Succeeded, Id=cac343e5-911b-4ba7-a0c7-69e883a61392, Duration=185014ms)
2022-02-06T01:02:14.293 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T01:02:14.2930303+00:00', Id=729e8079-e33e-48f4-8c5a-48e102a6d9d1)
2022-02-06T01:02:14.300 [Error] Executed 'Function1' (Failed, Id=729e8079-e33e-48f4-8c5a-48e102a6d9d1, Duration=1ms)e051565c-9900-4cef-9786-5795c9fc7f91: 3
2022-02-06T01:03:14.395 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T01:03:14.3956880+00:00', Id=5996df91-cecf-418f-a5b5-82d7680761f8)
2022-02-06T01:03:14.403 [Error] Executed 'Function1' (Failed, Id=5996df91-cecf-418f-a5b5-82d7680761f8, Duration=1ms)e051565c-9900-4cef-9786-5795c9fc7f91: 4
之后,由于重试策略的延迟时间超过了函数实例的闲置冷却时间(静态字典显然从内存中删除,因为实例已关闭),该方法继续失败。
这可能表明EventGrid忽略了3分钟之前调用的任何响应。也就是说,第一次调用的185秒延迟的“OK”响应被忽略了。所有随后的调用都失败了,导致事件排队重试。
除了这些简单测试显示的内容之外,可能还有更多内容。如果您确实必须知道,我建议在GitHub上开启一个问题票据。
消除幂等性问题的潜在解决方案
我不确定我完全理解您的“多部分步骤”。但是,如果您可以将该部分包装到单个会话中,则可以将EventGridTrigger
与启用会话的ServiceBusTrigger
链接起来。
如果您的事件具有唯一标识符(eventGridEvent.Id
),则可以使用与事件标识符匹配的会话ID构建服务总线消息。启用会话的服务总线触发器将确保每次只调用1个事件(或会话)。
这有效是因为两个相同的事件也共享相同的eventGridEvent.Id
。但是,您将不得不跟踪这些标识符。在服务总线会话结束时,您必须将该ID标记为“已处理”。在会话开始时,请检查是否先前已处理该ID;如果是,则忽略请求。
在此处阅读有关带“消息会话”的ServiceBusTrigger
和在此处(查看isSessionsEnabled
属性)(您可以搜索“session”以查找与会话相关的页面更多内容,它们分散在各处)。