Если вы не знакомы с 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. Тестирование государственного автомата очень необходимо.