Подход ContinueWith
отлично работал для меня.Мне удалось смоделировать версию вашего сценария, используя следующий код оркестратора:
[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)
, был , а не такой же, как поток, который вызвал функцию оркестратора.Я не знаю, почему мой первоначальный пример не ударил это, или почему ваша версия сделала.Это как-то связано с тем, как 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 фактически никогда не завершаются, пока не будут выполнены все нерешенные долговременные задачи.Нет никакого способа обойти это.Обратите внимание, что вы по-прежнему можете запускать другие экземпляры оркестратора, может возникнуть конфликт, если они окажутся в одном и том же разделе (по умолчанию 4 раздела).