Если вы не знакомы с AWS Step Functions, это сервис, в котором мы можем создавать конечные автоматы для управления асинхронными задачами в рабочих процессах, не беспокоясь о какой-либо базовой инфраструктуре и операциях. В последнее время я много использовал его и хотел бы поделиться тем, как я реализую блок finally
в конечном автомате Step Functions.
В рабочем процессе могут быть некоторые операции, которые необходимо выполнить независимо от того, что происходит, например, очистка временных ресурсов, которые создаются в начале рабочего процесса. Если мы относимся к языку Java, это похоже на то, что мы делаем с блоком finally
.
try { // Do something } catch (SomeException ex) { // Handle exception } finally { // The things that must happen no matter what }
В AWS Step Functions поведение по умолчанию, когда состояние сообщает об исключении, заключается в полном отказе выполнения, что означает, что остальная часть состояния в конечном автомате не будет выполнена. Step Functions предоставляет возможность перехвата исключений (см. Обработка ошибок в Step Functions), но для блока finally
нет явной функции.
Я решил, что мы могли бы использовать поле Catch
с подстановочным именем исключения States.ALL
для реализации блока finally
. Начнем с простого примера:
{ "Comment": "An simple example", "StartAt": "DoSomething", "States": { "DoSomething": { "Type": "Task", "Resource": "arn:aws:states:${region}:${account}:activity:DoSomethingActivity", "Next": "Cleanup" }, "Cleanup": { "Type": "Task", "Resource": "arn:aws:states:${region}:${account}:activity:CleanupActivity", "End": true } } }
Это конечный автомат, у которого есть только один шаг, а затем очистка, и у нас пока нет обработки ошибок.
Если состояние DoSomething
получает какое-либо исключение, либо вызванное изнутри действия, либо вызванное пошаговыми функциями из-за тайм-аута и т. Д., Состояние Cleanup
не будет выполнено. Теперь давайте реализуем наш finally
:
{ "Comment": "An simple example with Catch", "StartAt": "DoSomething", "States": { "DoSomething": { "Type": "Task", "Resource": "arn:aws:states:${region}:${account}:activity:DoSomethingActivity", "Catch": [ { "ErrorEquals": [ "States.ALL" ], "ResultPath": null, "Next": "Cleanup" } ], "Next": "Cleanup" }, "Cleanup": { "Type": "Task", "Resource": "arn:aws:states:${region}:${account}:activity:CleanupActivity", "End": true } } }
Я добавил поле Catch
в состоянии DoSomething
. Он использует подстановочный знак States.ALL
, чтобы перехватить почти все исключения и сделать Cleanup
резервным состоянием. Таким образом, Cleanup
будет выполняться независимо от DoSomething
успеха или неудачи.
Обратите внимание, что «catcher», как это называется в документации AWS, имеет поле ResultPath
, это связано с тем, что выходными данными исключения будут его данные диагностики, и обычно он не должен перезаписывать полезную нагрузку состояния. машина. Вы можете использовать "ResultPath": null"
, чтобы вообще не использовать вывод исключения, или использовать что-то вроде "ResultPath": "$.exception"
, чтобы поместить вывод исключения в определенное поле JSON полезной нагрузки.
Не так уж и плохо, правда? Но что, если рабочий процесс имеет 10 или даже 100 состояний?
Да, приведенный выше пример слишком упрощен, очень часто конечный автомат имеет несколько состояний. Один из простых способов - добавить один и тот же catcher в каждое отдельное состояние конечного автомата, но это будет слишком много повторений. Я хотел бы поделиться двумя вариантами, которые я нашел более элегантными. Но я должен указать, что, согласно синтаксису Amazon States Language (язык JSON для описания конечного автомата), только Task
, Parallel
и Map
состояния могут иметь Catch
поля. Надеюсь, вы сочтете следующие варианты разумными с учетом этого ограничения.
Вариант 1. Оберните тело рабочего процесса в состояние Parallel
, имеющее только одну ветвь
{ "Comment": "An simple example using Parallel", "StartAt": "Prepare", "States": { "Prepare": { "Type": "Task", "Resource": "arn:aws:states:${region}:${account}:activity:PrepareActivity", "Next": "WorkflowBody" }, "WorkflowBody": { "Type": "Parallel", "Branches": [ { "StartAt": "DoSomething", "States": { "DoSomething": { "Type": "Task", "Resource": "arn:aws:states:${region}:${account}:activity:DoSomethingActivity", "Next": "DoSomethingElse" }, "DoSomethingElse": { "Type": "Task", "Resource": "arn:aws:states:${region}:${account}:activity:DoSomethingElseActivity", "End": true } } } ], "Catch": [ { "ErrorEquals": [ "States.ALL" ], "ResultPath": null, "Next": "Cleanup" } ], "Next": "Cleanup" }, "Cleanup": { "Type": "Task", "Resource": "arn:aws:states:${region}:${account}:activity:CleanupActivity", "End": true } } }
В этом варианте конечный автомат имеет три состояния: Prepare
, WorkflowBody
и Cleanup
. WorkflowBody
инкапсулирует основные этапы рабочего процесса и имеет связанный ловушку. Следовательно, если шаги DoSomething
и DoSomethingElse
вызывают какое-либо исключение, оно будет перехвачено полем Catch
WorkflowBody
, и Cleanup
все равно будет выполнено.
Вариант 2. Создайте отдельный конечный автомат для тела рабочего процесса и выполните его из того, у которого есть ловушка.
{ "Comment": "An simple example with invoking another state machine", "StartAt": "Prepare", "States": { "Prepare": { "Type": "Task", "Resource": "arn:aws:states:${region}:${account}:activity:PrepareActivity", "Next": "WorkflowBody" }, "WorkflowBody": { "Type": "Task", "Resource": "arn:aws:states:::states:startExecution.sync", "Parameters": { "StateMachineArn": "arn:aws:states:${region}:${account}:stateMachine:WorkflowBody", "Input": { "AWS_STEP_FUNCTIONS_STARTED_BY_EXECUTION_ID.$": "$$.Execution.Id", "yourData.$": "$.fromPayload" } }, "Catch": [ { "ErrorEquals": [ "States.ALL" ], "ResultPath": null, "Next": "Cleanup" } ], "Next": "Cleanup" }, "Cleanup": { "Type": "Task", "Resource": "arn:aws:states:${region}:${account}:activity:CleanupActivity", "End": true } } }
В этом варианте для одного рабочего процесса будут определены два конечных автомата. Первый, который представляет собой приведенный выше код, определяет состояния подготовки и очистки и использует arn:aws:state:::states:startExecution.sync
для вызова второго конечного автомата. А второй конечный автомат будет содержать основные этапы рабочего процесса.
Эти два варианта работают хорошо, но я лично предпочитаю вариант 1. Основные недостатки варианта 2: (1) он потребляет больше квоты API StartExecution
, следовательно, более высокий риск дросселирования; (2) В нашем стеке больше частей инфраструктуры, включая несколько конечных автоматов, политику IAM для одного конечного автомата, вызывающего другой (см. Политики IAM) и т. Д.
Еще одна вещь, которую следует принять во внимание, - это то, что подстановочный знак States.ALL
не является надмножеством всех исключений, поэтому способ, который я показал в этой статье, технически не является реальным finally
. Как сказано в документации, States.Runtime
ошибки не будут обнаружены с помощью оператора Retry или Catch States.ALL
. Но, к счастью, States.Runtime
обычно вызывается неверно определенным вводом / выводом в конечном автомате. Я надеюсь, что проблема такого рода должна быть обнаружена вашими тестами, а не во время выполнения.
Заключение
В конечном автомате, определенном в AWS Step Functions, мы можем использовать поле Catch
с подстановочным знаком States.ALL
для выполнения шагов, которые должны произойти независимо от того, что, например, очистка ресурсов.
Когда в конечном автомате есть несколько состояний, один из возможных способов - инкапсулировать тело основного рабочего процесса в состоянии Parallel
с соответствующим улавливателем. Альтернативой является создание отдельного рабочего процесса и вызов одного из другого, что имеет некоторые недостатки.
Также важно отметить, что States.ALL
не перехватывает все исключения, например States.Runtime
. Тестирование государственного автомата очень необходимо.