Защита бизнес-эффекта от повторных попыток процесса

AWSBeginner
Практиковаться сейчас

Введение

Обработчик сохраняет заказ, но завершается с ошибкой до того, как его результат поступает в процесс. Вы защитите эту запись, чтобы повторная попытка или повторное выполнение сохраняли исходный заказ, а другой заказ по-прежнему мог успешно обрабатываться.

Сначала пройдите лабораторные работы Повторная попытка после сбоя шага и обработка постоянных ошибок, Предотвращение повторных заказов с помощью условных записей и Настройка и диагностика функции Lambda. В этой независимой виртуальной машине предоставлен обработчик без защиты; вы добавите её.

Связь с сертификацией

Эта лабораторная работа помогает на практике изучить следующие темы экзаменов.

  • Solutions Architect – Associate (SAA-C03) · Задача 2.1: Безопасные при повторах процессы и сохранение записанных результатов.
  • Developer – Associate (DVA-C02) · Задача 1.1: Безопасные при повторах процессы и сохранение записанных результатов.
  • DevOps Engineer – Professional (DOP-C02) · Задача 5.1: Базовая практика: Безопасные при повторах процессы и сохранение записанных результатов.
  • Solutions Architect – Professional (SAP-C02) · Задача 2.4: Базовая практика: Безопасные при повторах процессы и сохранение записанных результатов.

Защитите бизнес-запись с помощью стабильного ключа

На этом шаге разверните обработчик, который считает повторный идентичный заказ уже выполненной записью.

два выполнения и один бизнес-заказ

Разные имена выполнений могут относиться к одному бизнес-заказу. Конфликт при условной записи идентичного заказа возвращает результат, указывающий на дубликат, без замены этого заказа.

Используйте AWS View рядом с Terminal, чтобы сопоставить запросы CLI с фактическими ресурсами и результатами этой лабораторной работы. Сохраните предоставленные справочные данные.

Идентификатор заказа — это бизнес-ключ: он определяет нужный заказ, даже если попытки выполнения задачи или имена выполнений процесса различаются. attribute_not_exists(id) разрешает только первый PutItem. DynamoDB проверяет это условие атомарно. ReturnValuesOnConditionCheckFailure:ALL_OLD предоставляет существующий элемент, когда повторная попытка проигрывает состязание за выполнение условия. Возвращайте результат, указывающий на дубликат, только если существующий элемент совпадает с нужным заказом; при конфликтующем содержимом операция должна завершаться ошибкой, а не незаметно заменять заказ.

Предоставленный исходный вариант выполняет безусловную запись. Замените его приведённым ниже полным обработчиком с защитой. Он использует настроенное окружение SDK, а не личные учётные данные. Диагностические счётчики попыток отслеживают фактические вызовы отдельно от бизнес-записей. В синтетическом режиме сбоя после записи первая попытка вызывает исключение после записи через API сервиса, создавая неопределённость, с которой должна справиться повторная попытка. Режим сводки читает фактически сохранённый элемент.

Here-document с разделителем в кавычках записывает Python-файл буквально. Существующая точка входа обработчика Lambda остаётся app.handler.

cd /home/labex/project
cat > app.py <<'PYTHON'
import json
import os
import boto3
from botocore.exceptions import ClientError

class TransientOrderError(Exception):
    pass
class InvalidOrder(Exception):
    pass

def handler(event, context):
    print('WORKFLOW '+json.dumps(event,sort_keys=True))
    db=boto3.client('dynamodb')
    if event.get('stage')=='summary':
        item=db.get_item(
            TableName=os.environ['ORDERS_TABLE'],
            Key={'id':{'S':event['id']}},
        )['Item']
        result={'id':event['id'],'total_cents':int(item['total_cents']['N']),'completed':True}
        print('RESULT '+json.dumps(result,sort_keys=True))
        return result
    if event.get('mode')=='permanent':
        raise InvalidOrder('Order cannot be completed')
    quantity=event['quantity']
    if isinstance(quantity,bool) or not isinstance(quantity,int) or not 1<=quantity<=10:
        raise InvalidOrder('Invalid quantity')
    attempt=db.update_item(
        TableName=os.environ['ATTEMPTS_TABLE'],
        Key={'id':{'S':event['id']}},
        UpdateExpression='ADD attempts :one',
        ExpressionAttributeValues={':one':{'N':'1'}},
        ReturnValues='UPDATED_NEW',
    )['Attributes']['attempts']['N']
    if event.get('mode')=='flaky' and attempt=='1':
        raise TransientOrderError('Dependency temporarily unavailable')
    item={'id':{'S':event['id']},'quantity':{'N':str(quantity)},'total_cents':{'N':str(quantity*250+100)}}
    duplicate=False
    try:
        db.put_item(
            TableName=os.environ['ORDERS_TABLE'],
            Item=item,
            ConditionExpression='attribute_not_exists(id)',
            ReturnValuesOnConditionCheckFailure='ALL_OLD',
        )
    except ClientError as error:
        if (
            error.response['Error']['Code']!='ConditionalCheckFailedException'
            or error.response.get('Item')!=item
        ):
            raise
        duplicate=True
    if event.get('mode')=='after-write' and attempt=='1':
        raise TransientOrderError('Result delivery failed after the write')
    result={'id':event['id'],'quantity':quantity,'total_cents':quantity*250+100,'duplicate':duplicate}
    print('RESULT '+json.dumps(result,sort_keys=True))
    return result
PYTHON

try выполняет условную запись через API сервиса. Только реальное исключение ConditionalCheckFailedException с идентичным старым элементом считается дубликатом; остальные ошибки SDK вызываются повторно. Временное исключение возникает после решения о записи, поэтому процесс действительно повторяет работу, бизнес-элемент которой уже существует.

Упакуйте файл командой zip -j, которая исключает пути каталогов, и обновите предоставленный обработчик с помощью --zip-file fileb://, чтобы передать двоичные байты архива. Поле CodeSha256 в ответе идентифицирует развёрнутый архив. Само развёртывание не доказывает защиту от дубликатов; её проверит шаг с выполнением.

zip -j function.zip app.py
aws lambda update-function-code \
  --function-name labex-ev05-worker \
  --zip-file fileb://function.zip \
  --query CodeSha256 \
  --output text

Запустите проверку развёртывания перед созданием выполнений.

Подключите процесс с ограниченными разрешениями и числом повторных попыток

На этом шаге создайте независимую роль процесса и машину состояний, которая может повторить вызов обработчика после его сбоя вслед за записью.

Для Step Functions нужны доверие сервису states и отдельное разрешение InvokeFunction для конкретной функции. Обработчик сохраняет собственные разрешения DynamoDB; роль процесса только вызывает его. Присваивания в оболочке сохраняют возвращённые идентификаторы. --query и --output text выбирают значения для повторного использования, file:// читает буквальный JSON доверия, а оболочка вставляет ARN обработчика в документ разрешений.

cd /home/labex/project
WORKER_NAME=labex-ev05-worker
WORKER_ARN=$(aws lambda get-function-configuration \
  --function-name labex-ev05-worker \
  --query FunctionArn \
  --output text)
aws lambda get-function-configuration \
  --function-name labex-ev05-worker \
  --query '{Name:FunctionName,Role:Role,Runtime:Runtime,Timeout:Timeout}'
aws stepfunctions list-state-machines
cat > workflow-trust.json <<'JSON'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "states.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
JSON
ROLE_ARN=$(aws iam create-role \
  --role-name labex-ev05-workflow-role \
  --assume-role-policy-document file://workflow-trust.json \
  --query Role.Arn \
  --output text)
cat > workflow-invoke.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "lambda:InvokeFunction",
      "Resource": "$WORKER_ARN"
    }
  ]
}
EOF
aws iam put-role-policy \
  --role-name labex-ev05-workflow-role \
  --policy-name InvokeWorker \
  --policy-document file://workflow-invoke.json
aws iam get-role-policy --role-name labex-ev05-workflow-role --policy-name InvokeWorker

Пустой список машин подтверждает, что процесс из предыдущей виртуальной машины не используется. Разрешение использует ARN функции, а каждый Task — её обычное имя. Retry обрабатывает только TransientOrderError с интервалом в одну секунду, удвоением задержки и не более чем двумя повторными попытками после первого вызова; Catch направляет InvalidOrder в явное состояние Fail. ResultSelector сохраняет фактический Payload, ResultPath помещает его под saved, а OutputPath возвращает фактическую сводку.

Here-document с разделителем в кавычках записывает ASL буквально. jq --arg заменяет заполнители обработчика перед созданием машины.

cat > workflow-template.json <<'JSON'
{
  "StartAt": "CheckQuantity",
  "States": {
    "CheckQuantity": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.quantity",
          "NumericGreaterThan": 0,
          "Next": "StoreOrder"
        }
      ],
      "Default": "Rejected"
    },
    "StoreOrder": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "WORKER_NAME",
        "Payload.$": "$"
      },
      "ResultSelector": {
        "result.$": "$.Payload"
      },
      "ResultPath": "$.saved",
      "Retry": [
        {
          "ErrorEquals": [
            "TransientOrderError"
          ],
          "IntervalSeconds": 1,
          "BackoffRate": 2,
          "MaxAttempts": 2
        }
      ],
      "Catch": [
        {
          "ErrorEquals": [
            "InvalidOrder"
          ],
          "Next": "Rejected",
          "ResultPath": "$.failure"
        }
      ],
      "Next": "ReadSummary"
    },
    "ReadSummary": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "WORKER_NAME",
        "Payload": {
          "stage": "summary",
          "id.$": "$.saved.result.id"
        }
      },
      "OutputPath": "$.Payload",
      "End": true
    },
    "Rejected": {
      "Type": "Fail",
      "Error": "OrderRejected",
      "Cause": "Order could not be completed"
    }
  }
}
JSON
jq --arg worker "$WORKER_NAME" '.States.StoreOrder.Parameters.FunctionName=$worker | .States.ReadSummary.Parameters.FunctionName=$worker' workflow-template.json > workflow.json
MACHINE_ARN=$(aws stepfunctions create-state-machine \
  --name labex-ev05-orders \
  --type STANDARD \
  --role-arn "$ROLE_ARN" \
  --definition file://workflow.json \
  --query stateMachineArn \
  --output text)
aws stepfunctions describe-state-machine \
  --state-machine-arn "$MACHINE_ARN" \
  --query '{Name:name,Definition:definition}'

Определение, полученное через API сервиса, содержит избирательные Retry/Catch и две реальные задачи. AWS View показывает машину, пока ещё без выполнений и заказов. Запустите проверку конфигурации процесса.

Подтвердите единственную бизнес-запись при повторной попытке и новом выполнении

На этом шаге запустите сбой после записи, повторите тот же заказ и создайте другой заказ.

Имена выполнений идентифицируют запуски процесса; идентификаторы заказов определяют бизнес-эффекты. Первый запуск использует режим сбоя после записи. Цикл с ограниченным числом итераций читает статус, пока тот не перестанет быть RUNNING; $(...) захватывает вывод, а break завершает цикл. Проверьте выполнение, если после цикла оно всё ещё имеет статус RUNNING.

FIRST_ARN=$(aws stepfunctions start-execution \
  --state-machine-arn "$MACHINE_ARN" \
  --name after-write-order \
  --input '{"id":"protected-order","quantity":4,"mode":"after-write"}' \
  --query executionArn \
  --output text)
for attempt in $(seq 1 60); do
  STATUS=$(aws stepfunctions describe-execution \
    --execution-arn "$FIRST_ARN" \
    --query status \
    --output text)
  if test "$STATUS" != RUNNING; then break; fi
  sleep 2
done
aws stepfunctions describe-execution \
  --execution-arn "$FIRST_ARN" \
  --query '{Status:status,Output:output}'
aws stepfunctions get-execution-history \
  --execution-arn "$FIRST_ARN" \
  --query 'events[?type==`TaskFailed` || type==`TaskSucceeded`].{Type:type,Error:taskFailedEventDetails.error,Output:taskSucceededEventDetails.output}'
aws dynamodb get-item \
  --table-name labex-ev05-orders \
  --key '{"id":{"S":"protected-order"}}' \
  --query Item

Первый TaskFailed содержит TransientOrderError после фактической записи. Успешная повторная попытка возвращает duplicate:true; сводка завершается с общей суммой 1100. Единственный элемент protected-order имеет количество 4 и общую сумму 1100. Повторная попытка задачи завершилась успешно без второго принятого PutItem для этого бизнес-ключа.

Запустите новое выполнение с другим именем, но тем же идентификатором заказа и значениями:

REPEAT_ARN=$(aws stepfunctions start-execution \
  --state-machine-arn "$MACHINE_ARN" \
  --name repeated-order \
  --input '{"id":"protected-order","quantity":4,"mode":"after-write"}' \
  --query executionArn \
  --output text)
for attempt in $(seq 1 60); do
  STATUS=$(aws stepfunctions describe-execution \
    --execution-arn "$REPEAT_ARN" \
    --query status \
    --output text)
  if test "$STATUS" != RUNNING; then break; fi
  sleep 2
done
aws stepfunctions describe-execution \
  --execution-arn "$REPEAT_ARN" \
  --query '{Status:status,Output:output}'
aws stepfunctions get-execution-history \
  --execution-arn "$REPEAT_ARN" \
  --query 'events[?type==`TaskSucceeded`].taskSucceededEventDetails.output'

Этот новый запуск также возвращает результат хранения, указывающий на дубликат, и читает исходную сводку с суммой 1100. Новое имя выполнения не делает заказ новым бизнес-запросом. Реальные отказы из-за невыполненного условия сохраняют исходный элемент вместо его перезаписи.

Используйте другой идентификатор заказа, чтобы доказать, что защита не отклоняет независимую работу:

NEW_ARN=$(aws stepfunctions start-execution \
  --state-machine-arn "$MACHINE_ARN" \
  --name different-order \
  --input '{"id":"different-order","quantity":1,"mode":"normal"}' \
  --query executionArn \
  --output text)
for attempt in $(seq 1 60); do
  STATUS=$(aws stepfunctions describe-execution \
    --execution-arn "$NEW_ARN" \
    --query status \
    --output text)
  if test "$STATUS" != RUNNING; then break; fi
  sleep 2
done
aws stepfunctions describe-execution \
  --execution-arn "$NEW_ARN" \
  --query '{Status:status,Output:output}'
aws dynamodb scan --table-name labex-ev05-orders --query Items
aws logs filter-log-events \
  --log-group-name /aws/lambda/labex-ev05-worker \
  --query 'events[].message'

Другой заказ успешно обрабатывается с количеством 1 и суммой 350. Есть два бизнес-элемента, хотя при попытках хранения и чтении сводок произошло семь фактических вызовов обработчика. Журналы и результаты задач, полученные через API сервиса, показывают решения о дубликатах для исходного ключа. AWS View показывает три успешные сводки и оба сохранённых заказа.

Пример ниже показывает фактические сводки для повторной попытки и нового выполнения вместе с сохранённым исходным заказом и отдельным новым заказом.

AWS View показывает сохранённый исходный заказ и другой бизнес-заказ Это идемпотентная запись для проверенного идентичного запроса, а не обещание, что задачи выполняются ровно один раз.

Запустите проверку защиты бизнес-записи.

Удалите ресурсы процесса и синтетические результаты

На этом шаге удалите свою завершённую машину, роль процесса, заказ и журналы, сохранив предоставленные вспомогательные ресурсы.

Все три выполнения завершились. Удаление машины убирает её из списка активных машин. Удалите собственную политику роли перед удалением роли, затем удалите синтетический заказ и группу журналов обработчика, созданную вашим выполнением.

aws stepfunctions delete-state-machine --state-machine-arn "$MACHINE_ARN"
aws iam delete-role-policy --role-name labex-ev05-workflow-role --policy-name InvokeWorker
aws iam delete-role --role-name labex-ev05-workflow-role
aws dynamodb delete-item \
  --table-name labex-ev05-orders \
  --key '{"id":{"S":"protected-order"}}'
aws dynamodb delete-item \
  --table-name labex-ev05-attempts \
  --key '{"id":{"S":"protected-order"}}'
aws dynamodb delete-item \
  --table-name labex-ev05-orders \
  --key '{"id":{"S":"different-order"}}'
aws dynamodb delete-item \
  --table-name labex-ev05-attempts \
  --key '{"id":{"S":"different-order"}}'
aws logs delete-log-group --log-group-name /aws/lambda/labex-ev05-worker

Прочитайте успешно полученные списки ресурсов, чтобы доказать, что осталось:

aws stepfunctions list-state-machines
aws iam list-roles --query 'Roles[].RoleName'
aws dynamodb scan --table-name labex-ev05-orders --query Items
aws logs describe-log-groups --query logGroups
aws dynamodb scan --table-name labex-ev05-reference --query Items

Активных машин, заказов и групп журналов нет. Остаётся только предоставленная роль обработчика, а справочный элемент не изменился. Сохраните предоставленные обработчик и таблицы: эти ресурсы принадлежат подготовке среды, в отличие от созданных вами процесса и ресурсов. Сетевые ошибки или ошибки аутентификации никогда не доказывают удаление.

Удалите обычные файлы, созданные в этой лабораторной работе:

rm -f app.py function.zip workflow-trust.json workflow-invoke.json workflow-template.json workflow.json

AWS View показывает пустые списки машин, выполнений и заказов и сохранённый справочный элемент. Запустите проверку очистки перед завершением виртуальной машины.

Резюме

Вы защитили бизнес-записи через API сервиса стабильным ключом заказа, считали дубликатами только конфликты условной записи идентичных данных и проверили реальный сбой после первой записи. Повторная попытка и новое выполнение процесса сохранили исходный заказ; другой бизнес-ключ создал собственный результат. Вы проверили историю и данные сервиса, прежде чем удалить собственные ресурсы процесса и синтетические результаты.