0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS】ぼくのかんがえた集計の障害と復旧に備えたサーバレス注文受付システム

0
Posted at

はじめに

こんばんは、mirukyです。

注文受付って色々と考慮事項が多いと思います。特に障害に備えて、となると難易度が跳ね上がる印象です。今回はそんな注文受付システムについて、ある程度考慮したものをサクッとサーバレスで作っていこうと思います。

構成はAWS SAMで管理しています。ここでは集計障害からの復旧を扱い、受付連絡はデータベースへの模擬記録で検証します。再構築中は対象の注文を固定するため、受付を一時停止する感じです。

目次

  1. 注文を保存してから集計と通知処理へ進む
  2. SAMテンプレートで構成を定義してデプロイする
  3. 重複配信と集計側の障害を確認する
  4. 別の世代で集計を作り直す

1. 注文を保存してから集計と通知処理へ進む

注文の保存に成功した直後、イベント送信に失敗すると、集計側はその注文の存在を知りません。保存と送信を個別に呼ぶ構成には、このような二重書き込みの問題があります。そこで、注文データと送信待ちイベントをDynamoDBの同じトランザクションで保存します。この送信待ち領域がアウトボックスです。

注文と送信待ちイベントの保存から集計と模擬通知へ配信する構成

注文関数は、注文とアウトボックスを確定できた時点で受付成功を返します。以降は、DynamoDB Streamsがテーブルの変更を中継関数へ渡し、中継関数がEventBridgeへイベントを送る構成です。集計と通知には専用のSQSキューとLambda関数を用意し、集計の失敗によって通知まで再試行されることを避けます。

中継関数は PutEvents の項目ごとの結果を確認し、成功したイベントのアウトボックスを送信済みに更新します。イベントを送った直後、この更新より前に関数が停止すると、再試行で同じイベントが届きます。注文とイベントの保存を一括で確定しても配信の重複は起こるため、受信側での処理済みIDの確認が必要です。

集計関数は、注文IDから導出した業務イベントIDの保存と、件数・金額の加算を同じトランザクションで行います。同じイベントが再び届いても、処理済みIDを条件付きで保存できないため、二重加算を防げます。このように、繰り返し実行しても結果を変えない性質が冪等性です。

集計データには世代IDも付けます。処理済みかどうかを世代ごとに判定することで、通常処理では重複を除外し、再構築時には過去の注文を新しい世代へ一度ずつ加算できます。EventBridgeが配信時に付けるIDは、この判定には使いません。

通知関数も処理済みIDを保存します。ただし、今回の通知はDynamoDBへの模擬記録です。メールや外部APIへの送信まで一度だけにする保証はありません。実際の送信を追加する場合は、送信先が対応する冪等キーなどを使い、送信と記録の間で停止したときの扱いを設計する必要があります。

集計・通知の処理やEventBridgeの配信に失敗したイベントは、デッドレターキュー(DLQ)へ残します。中継関数用のSQS送信先には、失敗したバッチのメタデータが入り、元のストリームレコード本体は含まれません。元のイベントはアウトボックスに保存しているので、そこから再配送します。再構築の制御テーブルや監視用アラームを含むテンプレートと関数コードは、次のリポジトリから取得できます。

2. SAMテンプレートで構成を定義してデプロイする

2-1. 認証とローカル環境

AWS SAMは、Lambda関数やイベントソースなどをテンプレートで定義するためのフレームワークです。SAM CLIでビルドし、CloudFormationを通じてデプロイします。検証環境はSAM CLI 1.165.0、AWS CLI v2、Python 3.13で、リージョンは東京(ap-northeast-1)です。

デプロイ時の変更セット名を指定する補助処理は、SAM CLI 1.165.0を前提にしています。導入と認証の手順はリポジトリのREADMEを参照してください。AWSマネジメントコンソールにログインしていても、ローカルのAWS CLIには別途認証の設定が必要です。

デプロイには、使用するサービスのリソース作成・管理権限と、IAMロール・ポリシーの設定権限、iam:PassRole が必要です。注文の登録と集計APIの呼び出しには execute-api:Invoke を使います。対象と操作の範囲は、リポジトリの docs/permissions.md に記載しています。

APIの呼び出し、データの参照と書き込み、イベントの保存と再生などにAWSの利用料金が発生します。EventBridgeのアーカイブには、保持するデータの量に応じた料金もかかります。

# 実装を取得し、Pythonの依存関係と環境専用の設定を作ります。
git clone https://github.com/miruky/sam-outbox-recovery-lab.git
cd sam-outbox-recovery-lab
python3.13 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python scripts/prepare.py
source .run/deploy-env.sh

# 関数コードと状態機械の定義をSAM CLIでビルドします。
sam --version
sam build

SAM CLI 1.165.0でビルドが成功した端末出力

Build Succeeded が出力され、デプロイに使うファイルが .aws-sam/build に作成されました。画像では長いビルドログの末尾を表示しています。以降も、仮想環境を有効にした同じ端末で、リポジトリ直下からコマンドを実行してください。

prepare.py は、AWSリソース名、変更セット名、デプロイ設定を .run/ へ保存します。作成済みの環境を操作するときは、この設定を再利用してください。名前を生成し直すと操作対象が変わってしまいます。.run/ は実際の識別子やログを含むため、Gitの対象外です。

2-2. アウトボックスの追加で中継関数を起動する

デプロイの前に、中継関数を起動する条件を確認します。下のYAMLは template.yaml の抜粋で、ロールや名前パラメーターの定義は省略しています。デプロイにはリポジトリの完全版を使ってください。!Ref と !GetAtt は、組み込み関数の短縮記法です。

# 注文テーブルの新しいイメージから、アウトボックスのINSERTだけを受け取ります。
Transform: AWS::Serverless-2016-10-31
Resources:
  OrdersTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Ref OrdersTableName
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: pk
          AttributeType: S
        - AttributeName: sk
          AttributeType: S
      KeySchema:
        - AttributeName: pk
          KeyType: HASH
        - AttributeName: sk
          KeyType: RANGE
      StreamSpecification:
        StreamViewType: NEW_IMAGE

  RelayFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref RelayFunctionName
      CodeUri: src/
      Handler: relay.handler
      Runtime: python3.13
      Architectures: [arm64]
      MemorySize: 256
      Timeout: 30
      Role: !GetAtt RelayFunctionRole.Arn
      LoggingConfig:
        LogGroup: !Ref RelayFunctionLog
        LogFormat: Text
      Environment:
        Variables:
          ORDERS_TABLE: !Ref OrdersTable
          CONTROL_TABLE: !Ref ControlTable
          AGGREGATE_TABLE: !Ref AggregateTable
          NOTIFICATIONS_TABLE: !Ref NotificationsTable
          EVENT_BUS: !Ref OrderBus
      Events:
        OutboxStream:
          Type: DynamoDB
          Properties:
            Stream: !GetAtt OrdersTable.StreamArn
            StartingPosition: TRIM_HORIZON
            BatchSize: 10
            MaximumRetryAttempts: 3
            MaximumRecordAgeInSeconds: 600
            BisectBatchOnFunctionError: true
            FunctionResponseTypes: [ReportBatchItemFailures]
            DestinationConfig:
              OnFailure:
                Type: SQS
                Destination: !GetAtt RelayDLQ.Arn
            FilterCriteria:
              Filters:
                - Pattern: '{"eventName":["INSERT"],"dynamodb":{"NewImage":{"pk":{"S":["OUTBOX"]}}}}'

SAMは Events の定義からイベントソースマッピングを作成し、DynamoDB Streamsと中継関数を関連付けます。フィルターの条件は、追加された項目が pk = OUTBOX であることです。INSERT だけを対象にするので、送信済みへの更新によって中継関数が再び起動することはありません。中継の再試行で生じる重複には、1章の業務イベントIDによる判定を使います。

2-3. SQSの再試行と同時実行数を制御する

集計関数が失敗すると、SQSはメッセージを再び処理できる状態へ戻します。何度も失敗するメッセージを通常のキューへ残し続けないよう、受信回数の上限とDLQを設定します。キューはCloudFormation、関数と起動設定はSAMのリソース型で定義できます。

# 集計の失敗を専用DLQへ残し、SQS起点の同時実行を2件に制限します。
Resources:
  AggregateDLQ:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Ref AggregateDLQName
      SqsManagedSseEnabled: true
      MessageRetentionPeriod: 345600
      ReceiveMessageWaitTimeSeconds: 20

  AggregateQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Ref AggregateQueueName
      SqsManagedSseEnabled: true
      MessageRetentionPeriod: 86400
      ReceiveMessageWaitTimeSeconds: 20
      VisibilityTimeout: 60
      RedrivePolicy:
        deadLetterTargetArn: !GetAtt AggregateDLQ.Arn
        maxReceiveCount: 5

  AggregateFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref AggregateFunctionName
      CodeUri: src/
      Handler: consumer.handler
      Runtime: python3.13
      Architectures: [arm64]
      MemorySize: 256
      Timeout: 10
      Role: !GetAtt AggregateFunctionRole.Arn
      LoggingConfig:
        LogGroup: !Ref AggregateFunctionLog
        LogFormat: Text
      Environment:
        Variables:
          ORDERS_TABLE: !Ref OrdersTable
          CONTROL_TABLE: !Ref ControlTable
          AGGREGATE_TABLE: !Ref AggregateTable
          NOTIFICATIONS_TABLE: !Ref NotificationsTable
          EVENT_BUS: !Ref OrderBus
          CONSUMER_KIND: aggregate
      Events:
        Queue:
          Type: SQS
          Properties:
            Queue: !GetAtt AggregateQueue.Arn
            BatchSize: 5
            FunctionResponseTypes: [ReportBatchItemFailures]
            ScalingConfig:
              MaximumConcurrency: 2

可視性タイムアウトの間は、受信したメッセージがほかの処理から見えなくなります。ここでは関数の制限時間10秒に対して60秒に設定しました。AWSが推奨する6倍の時間を確保し、バッチウィンドウは既定の0秒を使っています。

ReportBatchItemFailures を有効にし、関数も失敗したメッセージのIDを返すことで、成功分を含むバッチ全体の再試行を避けます。MaximumConcurrency: 2 は、このキューから起動する関数の同時実行上限です。通知と再構築にも専用のキューと上限を設けています。

2-4. 再生イベントを通知の対象から除外する

EventBridgeは再生イベントに replay-name を追加します。通常処理はこの属性がないイベント、再構築は属性があるイベントを対象にすると、過去の注文を再生しても通知用キューへ届きません。通常の集計用ルールも、通知用と同じ条件です。アーカイブの保持期間は1日とします。

# バスのイベントをアーカイブへ保存し、通常配送と再構築の宛先を条件で決定します。
Resources:
  OrderBus:
    Type: AWS::Events::EventBus
    Properties:
      Name: !Ref OrderBusName

  OrderArchive:
    Type: AWS::Events::Archive
    Properties:
      ArchiveName: !Ref OrderArchiveName
      SourceArn: !GetAtt OrderBus.Arn
      RetentionDays: 1
      EventPattern:
        source: [lab.orders]
        detail-type: [OrderCreated]

  NotificationRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Ref NotificationRuleName
      EventBusName: !Ref OrderBus
      EventPattern:
        source: [lab.orders]
        detail-type: [OrderCreated]
        replay-name:
          - exists: false
      State: ENABLED
      Targets:
        - Id: Notification
          Arn: !GetAtt NotificationQueue.Arn
          DeadLetterConfig:
            Arn: !GetAtt DeliveryDLQ.Arn
          RetryPolicy:
            MaximumRetryAttempts: 3
            MaximumEventAgeInSeconds: 300

  RebuildRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Ref RebuildRuleName
      EventBusName: !Ref OrderBus
      EventPattern:
        source: [lab.orders]
        detail-type: [OrderCreated]
        replay-name:
          - exists: true
      State: ENABLED
      Targets:
        - Id: Rebuild
          Arn: !GetAtt RebuildQueue.Arn
          DeadLetterConfig:
            Arn: !GetAtt DeliveryDLQ.Arn
          RetryPolicy:
            MaximumRetryAttempts: 3
            MaximumEventAgeInSeconds: 300

イベントパターンに一致するだけでは、EventBridgeはSQSへ送信できません。SQS側のポリシーで、送信元ルールのARN(リソース識別子)とアカウントを指定して許可します。通信にTLSを使わない要求は拒否します。

# EventBridgeによる送信の許可を、通知用ルールに限定します。
Resources:
  NotificationQueuePolicy:
    Type: AWS::SQS::QueuePolicy
    Properties:
      Queues: [!Ref NotificationQueue]
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Deny
            Principal: '*'
            Action: sqs:*
            Resource: !GetAtt NotificationQueue.Arn
            Condition:
              Bool:
                aws:SecureTransport: 'false'
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sqs:SendMessage
            Resource: !GetAtt NotificationQueue.Arn
            Condition:
              ArnEquals:
                aws:SourceArn: !GetAtt NotificationRule.Arn
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId

このポリシーが許可するのは、EventBridgeから通知用キューへの送信です。Lambdaがキューから受信する権限は、関数の実行ロールに定義しています。EventBridgeがキューへ渡せなかったイベントは DeadLetterConfig のDLQへ、関数内で処理に失敗したメッセージは2-3の RedrivePolicy のDLQへ移ります。

2-5. 待機・照合・切替をステートマシンで管理する

再構築では、アーカイブへの反映と再生の完了を待ち、集計内容の照合を通過してから参照先を切り替えます。この順序を管理するのがStep Functionsのステートマシンです。SAMの AWS::Serverless::StateMachine は、DefinitionUri から状態定義を取得し、DefinitionSubstitutions で制御関数のARNを埋め込みます。受付停止や照合、世代更新の処理は src/controller.py に実装しています。

# 状態定義と制御関数をSAMで関連付けます。
Resources:
  RebuildStateMachine:
    Type: AWS::Serverless::StateMachine
    Properties:
      Name: !Ref RebuildStateMachineName
      Type: STANDARD
      DefinitionUri: statemachine/rebuild.asl.json
      DefinitionSubstitutions:
        ControllerArn: !GetAtt ControllerFunction.Arn
      Role: !GetAtt RebuildStateMachineRole.Arn

状態定義のうち、待機と切替判断に関わる箇所を抜粋します。COMPLETED で再生の完了を検知した後も、集計内容の照合を通過するまで切替処理へ進みません。

{
  "States": {
    "WaitForArchive": {
      "Type": "Wait",
      "TimestampPath": "$.archive_ready_at",
      "Next": "StartReplay"
    },
    "ReplayFinished": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.replay_state",
          "StringEquals": "COMPLETED",
          "Next": "VerifyGeneration"
        }
      ],
      "Default": "WaitForReplay"
    },
    "GenerationVerified": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.verified",
          "BooleanEquals": true,
          "Next": "ActivateGeneration"
        }
      ],
      "Default": "WaitForConsumers"
    }
  }
}

完全版には、制御関数の再試行と失敗時の処理、ステートマシン全体の時間上限も定義しています。この制御が旧世代を保持しながら復旧できるか、4章で確認します。

2-6. 配布用バケットと本体のデプロイ

本体のデプロイには、関数コードなどの配布物を置くS3バケットが必要です。公開アクセスを禁止したバケットを bootstrap.yaml に定義しているので、先にこのスタックをSAM CLIで作成します。

# 配布用バケットをSAM CLIで作成します。
sam deploy --config-file .run/samconfig.toml --config-env bootstrap

SAM CLIによる配布用スタックの作成成功

配布用スタックが作成されました。本体をデプロイすると、SAM CLIは設定ファイルに記載したバケットへ配布物をアップロードし、テンプレートから各リソースを作成します。ここまで用意した関数やキュー、テーブルを、コンソールで個別に作る必要はありません。

2-1で実行した .run/deploy-env.sh は、SAM CLIの変更セット名を事前生成した値に置き換える補助処理を有効にします。標準のオプションではなく、SAM CLI 1.165.0の内部APIに依存するため、指定バージョンで実行してください。デプロイ自体は sam deploy が行います。

本体の初回デプロイに続けて、同じテンプレートをもう一度適用します。next_changeset.py が新しくするのは変更セット名だけです。リソースに差分がなければ、--no-fail-on-empty-changeset により正常終了します。

# 本体を作成し、同じ定義の再適用では差分が出ないことを確認します。
sam deploy --config-file .run/samconfig.toml --config-env application
python scripts/next_changeset.py
sam deploy --config-file .run/samconfig.toml \
  --config-env application --no-fail-on-empty-changeset

# 初期世代と注文受付の制御レコードを作成します。
python scripts/lab.py initialize
python scripts/lab.py console-links

本体への再適用で差分がなく初期状態が0件である端末出力

再適用の結果は No changes to deploy でした。制御レコードの初期化後は、注文・集計・通知記録がすべて0件で、参照世代は initial、注文受付は OPEN です。ここから注文を登録し、件数と金額の変化を確認できます。

コンソールで確認する際は、.run/console-links.md のリンクを使います。自分の環境のリソースを開けるため、名前を探し直す必要はありません。AWSを変更する操作は端末で行い、コンソールでは作成結果や設定を参照します。

CloudFormationのスタックがCREATE_COMPLETEである状態

「ステータス」は「CREATE_COMPLETE」 です。テンプレートからスタックを作成できたことが確認できます。注文の登録から集計まで動くかどうかは、3章でデータを投入して確かめます。注文テーブルの設定も開き、中継関数へ変更を渡す条件を確認しておきます。

DynamoDBストリームがオンで表示タイプが新しいイメージである状態

「ストリームのステータス」は「オン」、「表示タイプ」は「新しいイメージ」 です。変更後の項目を中継関数へ渡すための設定で、アウトボックスの追加を対象とする条件は2-2のフィルターにあります。通知用ルールでは、再生イベントを受け付けない条件も確認します。

replay-nameのexistsがfalseであるイベントパターン

「イベントパターン」の「replay-name」に、「exists」が「false」という条件があります。2-4で定義した、再生属性のないイベントだけを通知へ渡す条件です。通常の集計用ルールもこの条件を使い、再構築用ルールは再生属性のあるイベントを受け付けます。

3. 重複配信と集計側の障害を確認する

3-1. 同じ注文イベントを追加配信する

初期状態の環境へ、100円から1,200円まで100円刻みの合成注文を12件登録します。lab.py はAWS CLIから認証情報を取得し、IAM認証を設定したHTTP APIへ署名付きのリクエストを送ります。--start は注文番号の開始値、--count は登録する件数です。

# 12件を登録し、非同期処理後の件数と金額を確認します。
python scripts/lab.py orders --start 1 --count 12
python scripts/lab.py status

12件の注文が集計され合計7800円と通知記録12件になった結果

12件の登録後、集計は12件・7,800円、通知記録も12件になりました。集計と通知は非同期なので、実行直後に反映されていなければ、数秒後に status を再実行してください。この12件を基準に、注文1のイベントをさらに3回配信します。--order 1 が対象の注文、--times 3 が追加配信の回数です。

# 注文1の業務イベントを3回追加配信します。
python scripts/lab.py duplicate --order 1 --times 3
python scripts/lab.py status
python scripts/lab.py queues --summary

同じ業務イベントを3回追加配信しても集計と通知記録が12件のままである結果

追加配信後も、集計は12件・7,800円、通知記録は12件のままでした。配信そのものを一度に制限するのではなく、1章の処理済みIDによって同じ世代への二重加算を防いでいます。数秒後にも状態を確認し、集計用・通知用キューの visible と in_flight が0になったことを確認してから、障害を発生させます。

3-2. 集計を止めて8件追加する

現在の集計と通知記録は、ともに12件です。集計関数だけで例外を発生させる故障フラグを有効にし、注文13から20までの8件を追加します。注文の保存と通知処理が継続するか、集計値と比較します。

# 集計側の故障フラグを有効にし、注文13から20を追加します。
python scripts/lab.py fault on
python scripts/lab.py orders --start 13 --count 8
python scripts/lab.py status

集計側の故障により注文20件と通知記録20件に対して集計が12件で止まった結果

注文とアウトボックスは20件、通知記録も20件へ進みました。集計は12件・7,800円のままです。集計側で例外が発生しても、注文の保存と通知用キューの処理は継続しています。

集計に失敗したメッセージは、2-3で設定した受信回数の上限を超えるとDLQへ移ります。数分待ってからキューの件数を確認してください。SQSの表示件数は近似値であり、配信が重複すると追加した8件と一致しない場合もあります。復旧の成否には、この件数ではなく4章の内容照合を使います。

# 再試行後の各キューの件数を確認します。
python scripts/lab.py queues --summary
python scripts/lab.py console-links

.run/console-links.md からキューの確認先を開き、ファイルに記載された集計用DLQの名前を使って対象を特定します。再試行を終えたメッセージが、このDLQへ移ったかを確認します。

キューの利用可能なメッセージが8で処理中のメッセージが0である状態

「利用可能なメッセージ」は「8」、「処理中のメッセージ」は「0」 でした。集計用DLQに8件残っていることが確認できます。EventBridgeからキューへの送信は成功し、その後の関数内で集計に失敗したため、配信用DLQではなく処理用DLQへ移っています。

4. 別の世代で集計を作り直す

4-1. 再生の範囲と切替の条件

今回の故障フラグによる障害なら、解除後にSQSのDLQ再処理機能で未処理分を元のキューへ戻す方法もあります。ここでは、集計を一から作り直す手順として、全20件を新しい世代へ再計算します。作成途中の集計を返さないよう、内容の照合が終わるまでは旧世代を参照します。

アーカイブを元のイベントバスへ再生し新世代の集計を照合して参照世代を切り替える構成

EventBridgeのアーカイブは、元のイベントバスへ再生します。送信先の Destination.FilterArns に再構築用ルールだけを指定すると、再生イベントはそのルールから専用キューへ届きます。2-4の通常ルールで再生を除外する設定に加え、通常の集計・通知関数も再生属性を検査して拒否します。

ステートマシンは最初に注文受付を止め、アウトボックスがすべて送信済みであることを確認します。その上で、復元すべきイベントIDと金額の対応を固定します。注文保存のトランザクションにも受付状態の条件があるため、停止と競合した新規注文を追加できません。受付を継続しながら差分を取り込む方式は、今回は扱いません。

アーカイブへの反映には遅延があるため、AWSは再生まで10分待つことを推奨しています。この実装では、最後にアウトボックスを送信してから少なくとも10分経過するまで待機します。再生が完了した後もSQSの処理は続くため、集計内容が一致するまで待ちます。更新が遅れるアーカイブの表示件数は、完了判定に使いません。

# 故障フラグを解除し、新しい集計世代の再構築を開始します。
python scripts/lab.py fault off
python scripts/lab.py rebuild
python scripts/lab.py execution
python scripts/lab.py status

再構築がRUNNINGとなり注文受付がPAUSEDで旧世代の集計を参照している結果

再構築は RUNNING、受付は PAUSED になりました。参照世代は initial のままで、集計は12件です。新世代を作成している間も、APIは旧世代を参照します。停止中に注文21を送ると拒否されるはずなので、1件登録して確認します。

# 受付停止中の新規注文が拒否されることを確認します。
python scripts/lab.py orders --start 21 --count 1

受付停止中の注文がHTTP 503で拒否された結果

HTTP 503と order intake paused が返り、注文21は登録されませんでした。ここでは受付停止の確認として拒否させているため、コマンドの終了コード1が想定した結果です。再構築の対象は20件のままなので、そのイベントIDと金額を照合できます。

4-2. 照合済みの世代だけを参照先へ切り替える

件数と合計だけを比べると、1件の欠落と別の1件の混入や、注文間での金額の入れ替わりを見落とします。照合の対象は、イベントIDと金額の対応、件数、合計です。一致した時点で新世代への書き込みを停止し、強い整合性を指定してデータを再取得します。

照合を通過すると、参照先の更新、新世代の有効化、旧世代の退役、ジョブの確定を同じトランザクションで行います。切替の途中で一部だけが更新されることを防ぐためです。内容の不一致や待機上限によって再構築が失敗した場合は、旧世代を保持し、注文受付も停止したままにします。

# 完了するまで実行状態を確認し、今回の実行に対応する確認先を作ります。
python scripts/lab.py execution
python scripts/lab.py console-links

execution は今回の再構築の実行状態を返します。実行中なら時間を置いて再実行してください。console-links も再度実行すると、この再構築の確認先が追加され、ブラウザーから進捗を確認できます。

実行ステータスが成功で実行タイプがStandardである状態

「実行ステータス」は「成功」、「実行タイプ」は「Standard」 でした。ステートマシンの実行が終了しています。照合した件数と金額、切替後の参照世代、通知記録の件数を端末でも取得し、復旧の結果を確認します。

# 照合結果と、現在参照している世代の集計値を確認します。
python scripts/lab.py execution
python scripts/lab.py status

照合済みの20件と合計21000円が新世代へ切り替わり通知記録は20件のままである結果

照合結果は20件・21,000円で、切替結果は ACTIVATED でした。参照世代が新しくなり、集計にも同じ値が反映されています。通知記録は20件のままなので、再生によって模擬通知が追加されていません。注文受付を再開し、APIが新世代の集計を返すかを確認します。

# 注文受付を再開し、HTTP APIが新世代の集計を返すことを確認します。
python scripts/lab.py resume
python scripts/lab.py read-api
python scripts/lab.py status

受付再開後のAPIが20件と合計21000円を返し通知記録が20件である結果

受付は OPEN に戻り、APIは20件・21,000円を返しました。通知記録も20件のままです。失敗した8件だけを追加するのではなく、確定した20件を新しい世代へ再計算し、内容の一致を確認してから参照先を変更できました。

掲載した実装では、注文上限1,000件、再構築開始2回、アーカイブ保持1日の設定です。保持期間を過ぎた注文イベントは、この再生手順では復元できません。再生順序を保証する構成でもないため、単一リージョンでの加算集計を対象にしています。順序が意味を持つ状態遷移や、受付を止められない業務へ適用する場合は、追加の設計が必要です。

DynamoDB Streamsのレコード保持期間は24時間です。中継の停止が長引いた場合に備え、アウトボックス自体は残しています。未送信のイベントは、リポジトリの recover-outbox 操作で再配送できます。

おわりに

ここまでお付き合いいただきありがとうございます。

集計側で障害を発生させても注文の保存と模擬通知は継続し、過去のイベントから20件・21,000円の集計を復元できました。まぁこれはかなりミニマルなシステムではありますが、、。

ではまた、お会いしましょう。

参考リンク

注文の確定とイベント配信

アーカイブと再生

SAMと非同期処理の設定

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?