Amazon Connectから呼び出すLambdaのタイムアウト対策(DynamoDBを使ったステータス管理)

2024.04.26

はじめに

Amazon Connectから呼び出すAWS Lambdaのタイムアウト対策を、DynamoDBを使ったステータス管理で実現する方法をまとめました。

Amazon ConnectのフローからLambda関数を呼ぶ際、タイムアウトは最大8秒という制限があります。

Amazon Connectと自然言語処理(NLP)や生成AIを組み合わせて、電話の無人対応システムを構築することがあります。しかし、これらの高度な言語モデルの推論には時間がかかるため、8秒のタイムアウト制限を超えるケースがあります。

例えば、以下の生成AIを利用してLambdaの処理を実行する場合、タイムアウトすることがありました。

タイムアウト対策として、例えば以下が考えられます。

  • 1つのLambdaではなく、処理内容を分割して複数のLambdaを呼び出す
  • タイムアウトすることを前提に、Lambdaの実行が完了したかどうかDynamoDBでステータス管理する

今回は、後者の構築方法を解説します。

構築内容は以下です。

  • Lambda
    • 生成AIへのリクエストや時間のかかる内容を処理するLambda
    • 処理用Lambdaが実行完了したかどうか、ステータス確認用のLambda
  • DynamoDB
    • 処理用のLambdaが実行完了したかステータスを管理するテーブル
  • Connectフロー

DynamoDB

DynamoDBには、Lambdaから以下の2つの属性を保存します。

  • コンタクトID (contact_id)
    • 各通話を一意に識別するID
  • ステータス (execution_status)
    • Lambdaの実行状況を表す値で、実行中(in_progress)または、実行完了(completed)のいずれかを取ります。

テーブルは以下の設定で作成します

  • テーブル名:lambda_execution_status
  • パーティションキー:contact_id

Lambda作成

以下の2つのLambdaを作成します。

  • lambda_execution
    • 生成AIへのリクエストや時間のかかる内容を処理する
  • lambda_execution_status_check
    • lambda_executionが実行完了したかDynamoDBからステータスを確認する

lambda_execution

  • ランタイム:Python3.12
  • タイムアウト:30秒
  • IAMポリシーは、以下を追加します。
    • AmazonDynamoDBFullAccess
import boto3
import json
import time

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('lambda_execution_status')

def update_status_in_progress(contact_id):
    table.update_item(
        Key={
            'contact_id': contact_id
        },
        UpdateExpression='SET execution_status = :execution_status',
        ExpressionAttributeValues={
            ':execution_status': "in_progress"
        }
    )

def update_status_completed(contact_id):
    # 実際には、status以外の属性も更新する想定
    table.update_item(
        Key={
            'contact_id': contact_id
        },
        UpdateExpression='SET execution_status = :execution_status',
        ExpressionAttributeValues={
            ':execution_status': "completed"
        }
    )

def lambda_handler(event, context):
    print('event:' + json.dumps(event, ensure_ascii=False))
    contact_id = event["Details"]["ContactData"]["ContactId"]

    update_status_in_progress(contact_id)
    
    # 何かしらの処理
    time.sleep(1)
    
    update_status_completed(contact_id)
    
    return {
        'status': 'completed'
    }
  • DynamoDBのテーブル名は、各自変えてください。
  • 通話中にConnectフロー内でループブロックを利用する可能性があるため、update_status_in_progressの通り、updateという関数名にしています。
  • コメントに記載通り、2つの関数の間に何かしらの処理(時間のかかる処理)を入れる想定です。必要な処理を追加してください。

lambda_execution_status_check

  • ランタイム:Python3.12
  • タイムアウト:8秒
  • IAMポリシーは、以下を追加します。
    • AmazonDynamoDBReadOnlyAccess
import boto3
import json
import logging
import time

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('lambda_execution_status')

logger = logging.getLogger()
logger.setLevel(logging.INFO)

RETRY_DELAY_SECONDS = 1

def get_execution_status(contact_id):
    response = table.get_item(
        Key={
            'contact_id': contact_id
        }
    )
    return response['Item']['execution_status']

def lambda_handler(event, context):
    print('event:' + json.dumps(event, ensure_ascii=False))

    contact_id = event["Details"]["ContactData"]["ContactId"]

    while True:
        try:
            execution_status = get_execution_status(contact_id)
        except KeyError:
            logger.error(f"Invalid execution status for contact_id: {contact_id}")
            return {
                'status': 'error'
            }

        if execution_status == 'completed':
            return {
                'status': execution_status
            }
        elif execution_status == 'in_progress':
            time.sleep(RETRY_DELAY_SECONDS)
            continue

        logger.error(f"Unexpected execution status: {execution_status} for contact_id: {contact_id}")
        return {
            'status': 'error'
        }
  • DynamoDBのテーブル名は、各自変えてください。
  • DynamoDBに保存された属性名execution_status(Lambdaのステータス)の属性値であるin_progresscompletedになるまで、1秒毎に確認します。
  • ステータスがin_progressもしくはcompleted以外の値であれば、errorを返します。
  • contact_idが存在しなければ、errorを返します。
  • Lambdaのタイムアウト設定を8秒にしていますので、8秒間completedにならなければ終了します。

Connectフロー

以下が全体のフローになります。

フロー内容 (クリックすると展開します)
{
  "Version": "2019-10-30",
  "StartAction": "8630447c-510e-47c9-b2aa-b2f3aa2452f9",
  "Metadata": {
    "entryPointPosition": {
      "x": -17.6,
      "y": 52
    },
    "ActionMetadata": {
      "20016ab2-ab8f-45a9-979e-fbd1cc11ad5c": {
        "position": {
          "x": 257.6,
          "y": 217.6
        },
        "conditionMetadata": [],
        "countryCodePrefix": "+1"
      },
      "8630447c-510e-47c9-b2aa-b2f3aa2452f9": {
        "position": {
          "x": 83.2,
          "y": 2.4
        }
      },
      "6b6a5984-8096-4946-987b-54537088a266": {
        "position": {
          "x": 290.4,
          "y": 4
        },
        "children": [
          "61f59d65-4895-47de-855b-3f6d07fa90a6"
        ],
        "overrideConsoleVoice": true,
        "fragments": {
          "SetContactData": "61f59d65-4895-47de-855b-3f6d07fa90a6"
        },
        "overrideLanguageAttribute": true
      },
      "61f59d65-4895-47de-855b-3f6d07fa90a6": {
        "position": {
          "x": 290.4,
          "y": 4
        },
        "dynamicParams": []
      },
      "69b3180b-f876-488f-998f-723d37f28153": {
        "position": {
          "x": 44.8,
          "y": 217.6
        },
        "toCustomer": false,
        "fromCustomer": true
      },
      "ace21dc3-9b85-41e2-a0eb-32aaea2c0085": {
        "position": {
          "x": 500.8,
          "y": 5.6
        }
      },
      "17b3d6aa-cb41-4601-b8fd-47a19c08dce2": {
        "position": {
          "x": 718.4,
          "y": 3.2
        },
        "parameters": {
          "PromptId": {
            "displayName": "Beep.wav"
          }
        },
        "promptName": "Beep.wav"
      },
      "d1aa2cd0-2c59-4321-a479-c83670e51601": {
        "position": {
          "x": 477.6,
          "y": 209.6
        }
      },
      "5bf35837-3aad-4374-91f1-9ca0c27c5afc": {
        "position": {
          "x": 718.4,
          "y": 215.2
        }
      },
      "691a5ab8-3cb2-43b4-9ecf-37028119ef6f": {
        "position": {
          "x": 234.4,
          "y": 838.4
        }
      },
      "2e6627fa-f5fc-4f6c-b018-72d6f3624689": {
        "position": {
          "x": 1320,
          "y": 888
        }
      },
      "358abe17-1a67-44f7-9f26-7f3c5c8d8cfb": {
        "position": {
          "x": 872,
          "y": 643.2
        }
      },
      "ddc0145a-ac98-445b-bb5a-b1aad3dc1441": {
        "position": {
          "x": 1106.4,
          "y": 643.2
        }
      },
      "49007031-591c-41e6-a446-ad9ac799e081": {
        "position": {
          "x": 40,
          "y": 440.8
        },
        "parameters": {
          "LambdaFunctionARN": {
            "displayName": "lambda_execution"
          }
        },
        "dynamicMetadata": {}
      },
      "622b8584-14e6-4351-89a1-735b109374e1": {
        "position": {
          "x": 450.4,
          "y": 839.2
        },
        "parameters": {
          "LambdaFunctionARN": {
            "displayName": "lambda_execution_status_check"
          }
        },
        "dynamicMetadata": {}
      },
      "6b04c75b-44fb-467b-a000-14261649124f": {
        "position": {
          "x": 1104.8,
          "y": 452.8
        }
      },
      "a4500abf-c95a-42a1-a109-0630b5447be1": {
        "position": {
          "x": 667.2,
          "y": 438.4
        },
        "conditionMetadata": [
          {
            "id": "881d35f2-4873-40a4-b95b-722c68037093",
            "operator": {
              "name": "Equals",
              "value": "Equals",
              "shortDisplay": "="
            },
            "value": "completed"
          },
          {
            "id": "7a3b2b28-d522-4559-9b3c-ceb3b1f4c0c7",
            "operator": {
              "name": "Equals",
              "value": "Equals",
              "shortDisplay": "="
            },
            "value": "error"
          }
        ]
      }
    },
    "Annotations": [],
    "name": "lambda_status",
    "description": "",
    "type": "contactFlow",
    "status": "published",
    "hash": {}
  },
  "Actions": [
    {
      "Parameters": {
        "StoreInput": "True",
        "InputTimeLimitSeconds": "60",
        "Text": "、",
        "DTMFConfiguration": {
          "DisableCancelKey": "False"
        },
        "InputValidation": {
          "CustomValidation": {
            "MaximumLength": "1"
          }
        }
      },
      "Identifier": "20016ab2-ab8f-45a9-979e-fbd1cc11ad5c",
      "Type": "GetParticipantInput",
      "Transitions": {
        "NextAction": "d1aa2cd0-2c59-4321-a479-c83670e51601",
        "Errors": [
          {
            "NextAction": "d1aa2cd0-2c59-4321-a479-c83670e51601",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "FlowLoggingBehavior": "Enabled"
      },
      "Identifier": "8630447c-510e-47c9-b2aa-b2f3aa2452f9",
      "Type": "UpdateFlowLoggingBehavior",
      "Transitions": {
        "NextAction": "6b6a5984-8096-4946-987b-54537088a266"
      }
    },
    {
      "Parameters": {
        "TextToSpeechEngine": "Neural",
        "TextToSpeechStyle": "None",
        "TextToSpeechVoice": "Kazuha"
      },
      "Identifier": "6b6a5984-8096-4946-987b-54537088a266",
      "Type": "UpdateContactTextToSpeechVoice",
      "Transitions": {
        "NextAction": "61f59d65-4895-47de-855b-3f6d07fa90a6"
      }
    },
    {
      "Parameters": {
        "LanguageCode": "ja-JP"
      },
      "Identifier": "61f59d65-4895-47de-855b-3f6d07fa90a6",
      "Type": "UpdateContactData",
      "Transitions": {
        "NextAction": "ace21dc3-9b85-41e2-a0eb-32aaea2c0085",
        "Errors": [
          {
            "NextAction": "ace21dc3-9b85-41e2-a0eb-32aaea2c0085",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "MediaStreamingState": "Enabled",
        "MediaStreamType": "Audio",
        "Participants": [
          {
            "ParticipantType": "Customer",
            "MediaDirections": [
              "From"
            ]
          }
        ]
      },
      "Identifier": "69b3180b-f876-488f-998f-723d37f28153",
      "Type": "UpdateContactMediaStreamingBehavior",
      "Transitions": {
        "NextAction": "20016ab2-ab8f-45a9-979e-fbd1cc11ad5c",
        "Errors": [
          {
            "NextAction": "20016ab2-ab8f-45a9-979e-fbd1cc11ad5c",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "Text": "発話後、1を入力ください。"
      },
      "Identifier": "ace21dc3-9b85-41e2-a0eb-32aaea2c0085",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "17b3d6aa-cb41-4601-b8fd-47a19c08dce2",
        "Errors": [
          {
            "NextAction": "17b3d6aa-cb41-4601-b8fd-47a19c08dce2",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "PromptId": "arn:aws:connect:ap-northeast-1:xxxxxxxxxxxx:instance/3ff2093d-af96-43fd-b038-3c07cdd7609c/prompt/54bb3277-0484-45eb-bdc7-2e0b1af31b5c"
      },
      "Identifier": "17b3d6aa-cb41-4601-b8fd-47a19c08dce2",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "69b3180b-f876-488f-998f-723d37f28153",
        "Errors": [
          {
            "NextAction": "69b3180b-f876-488f-998f-723d37f28153",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "MediaStreamingState": "Disabled",
        "Participants": [
          {
            "ParticipantType": "Customer",
            "MediaDirections": [
              "To",
              "From"
            ]
          }
        ],
        "MediaStreamType": "Audio"
      },
      "Identifier": "d1aa2cd0-2c59-4321-a479-c83670e51601",
      "Type": "UpdateContactMediaStreamingBehavior",
      "Transitions": {
        "NextAction": "5bf35837-3aad-4374-91f1-9ca0c27c5afc",
        "Errors": [
          {
            "NextAction": "5bf35837-3aad-4374-91f1-9ca0c27c5afc",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "Text": "メッセージをお預かりしました。"
      },
      "Identifier": "5bf35837-3aad-4374-91f1-9ca0c27c5afc",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "49007031-591c-41e6-a446-ad9ac799e081",
        "Errors": [
          {
            "NextAction": "49007031-591c-41e6-a446-ad9ac799e081",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "Text": "少々お待ちください。"
      },
      "Identifier": "691a5ab8-3cb2-43b4-9ecf-37028119ef6f",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "622b8584-14e6-4351-89a1-735b109374e1",
        "Errors": [
          {
            "NextAction": "622b8584-14e6-4351-89a1-735b109374e1",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {},
      "Identifier": "2e6627fa-f5fc-4f6c-b018-72d6f3624689",
      "Type": "DisconnectParticipant",
      "Transitions": {}
    },
    {
      "Parameters": {
        "LoopCount": "2"
      },
      "Identifier": "358abe17-1a67-44f7-9f26-7f3c5c8d8cfb",
      "Type": "Loop",
      "Transitions": {
        "NextAction": "ddc0145a-ac98-445b-bb5a-b1aad3dc1441",
        "Conditions": [
          {
            "NextAction": "691a5ab8-3cb2-43b4-9ecf-37028119ef6f",
            "Condition": {
              "Operator": "Equals",
              "Operands": [
                "ContinueLooping"
              ]
            }
          },
          {
            "NextAction": "ddc0145a-ac98-445b-bb5a-b1aad3dc1441",
            "Condition": {
              "Operator": "Equals",
              "Operands": [
                "DoneLooping"
              ]
            }
          }
        ]
      }
    },
    {
      "Parameters": {
        "Text": "エラーとなりました。電話をきります。"
      },
      "Identifier": "ddc0145a-ac98-445b-bb5a-b1aad3dc1441",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "2e6627fa-f5fc-4f6c-b018-72d6f3624689",
        "Errors": [
          {
            "NextAction": "2e6627fa-f5fc-4f6c-b018-72d6f3624689",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "LambdaFunctionARN": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:lambda_execution",
        "InvocationTimeLimitSeconds": "8",
        "ResponseValidation": {
          "ResponseType": "JSON"
        }
      },
      "Identifier": "49007031-591c-41e6-a446-ad9ac799e081",
      "Type": "InvokeLambdaFunction",
      "Transitions": {
        "NextAction": "a4500abf-c95a-42a1-a109-0630b5447be1",
        "Errors": [
          {
            "NextAction": "691a5ab8-3cb2-43b4-9ecf-37028119ef6f",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "LambdaFunctionARN": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:lambda_execution_status_check",
        "InvocationTimeLimitSeconds": "8",
        "ResponseValidation": {
          "ResponseType": "JSON"
        }
      },
      "Identifier": "622b8584-14e6-4351-89a1-735b109374e1",
      "Type": "InvokeLambdaFunction",
      "Transitions": {
        "NextAction": "a4500abf-c95a-42a1-a109-0630b5447be1",
        "Errors": [
          {
            "NextAction": "a4500abf-c95a-42a1-a109-0630b5447be1",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "Text": "ラムダの実行が完了しました。電話を切ります。"
      },
      "Identifier": "6b04c75b-44fb-467b-a000-14261649124f",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "2e6627fa-f5fc-4f6c-b018-72d6f3624689",
        "Errors": [
          {
            "NextAction": "2e6627fa-f5fc-4f6c-b018-72d6f3624689",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "ComparisonValue": "$.External.status"
      },
      "Identifier": "a4500abf-c95a-42a1-a109-0630b5447be1",
      "Type": "Compare",
      "Transitions": {
        "NextAction": "358abe17-1a67-44f7-9f26-7f3c5c8d8cfb",
        "Conditions": [
          {
            "NextAction": "6b04c75b-44fb-467b-a000-14261649124f",
            "Condition": {
              "Operator": "Equals",
              "Operands": [
                "completed"
              ]
            }
          },
          {
            "NextAction": "ddc0145a-ac98-445b-bb5a-b1aad3dc1441",
            "Condition": {
              "Operator": "Equals",
              "Operands": [
                "error"
              ]
            }
          }
        ],
        "Errors": [
          {
            "NextAction": "358abe17-1a67-44f7-9f26-7f3c5c8d8cfb",
            "ErrorType": "NoMatchingCondition"
          }
        ]
      }
    }
  ]
}

フロー内の[メディアストリーミング]ブロックは、今回のLambdaのステータス管理とは直接関係ありません。このブロックは、通話の録音内容を文字起こしや自然言語処理に利用することを想定しています。

Lambda 関数を呼び出す

[Lambda 関数を呼び出す]は、フローに2つ存在します。

1つ目の処理用のLambda(lambda_execution)でタイムアウトした場合、2つ目のステータス確認用のLambda(lambda_execution_status_check)に遷移します。

処理用のLambda(lambda_execution)でタイムアウトせずに実行完了した場合、ステータス確認用のLambdaには遷移しません。

今回は、2つのLambdaをどちらも最大タイムアウトを8秒と設定していますので、8秒間は無音になる可能性があります。

そこで、タイムアウト時間を短くし、Lambdaの後に[プロンプトの再生]ブロックを用いることで、実行中であることを伝えることができ、ユーザー体験が良くなるケースがあります。実際に電話で試して調整することをおすすめします。

コンタクト属性を確認する

ステータスを確認するLambda (lambda_execution_status_check)は、ステータスがcompletedになるまで1秒ごとに確認します。そのため、ステータスがcompletedにならない場合、タイムアウトする可能性があります。

そこで、[コンタクト属性を確認する]ブロックでは以下の通り、statusが一致しない場合に、再度ループするようにフローを設定しています。

通話後

通話終了後、DynamoDBにステータスとコンタクトIDが正しく保存されていることを確認できました。

通話中に処理用のLambda(lambda_execution)が完了する前に電話を切った場合、DynamoDBのステータスはin_progressのままとなります。その後、Lambdaの処理が完了すると、ステータスがcompletedに更新されます。

最後に

今回は、Amazon Connectから呼び出すLambdaのタイムアウト対策として、DynamoDBでステータス管理をしてみました。

他にも方法があると思いますが、1つの方法として参考になれば幸いです。