Redshift Data APIのレスポンスを様々な異常系パターンで検証する

本記事では、Redshift Data APIの異常系レスポンスを調べていきます。先にパターン別の結果を載せておきます。

  • テーブルの中身が0件
    • get_statement_resultのレスポンスのRecordsが空のリスト
    • Pythonは正常終了
  • テーブルが存在しない場合
    • describe_statementのレスポンスのStatusErrorにエラー情報
    • Pythonは正常終了
  • SQLのシンタックスエラー
    • describe_statementのレスポンスのStatusErrorにエラー情報
    • Pythonは正常終了
  • パラメータ間違え
    • DatabaseDbUser
      • describe_statementのレスポンスのStatusErrorにエラー情報
      • Pythonは正常終了
    • ClusterIdentifier
      • エラーメッセージと共にPythonがエラー終了
  • パラメータ不足
    • エラーメッセージと共にPythonがエラー終了
  • クラスタが停止中
    • エラーメッセージと共にPythonがエラー終了

Redshiftは最近のアップデートで、JDBCドライバーがなくてもSQLを投げれるようになりました。

非同期APIのため、クエリを実行し結果を取得するまで数回の手続きが発生します。その流れをおさらいしながら、異常時にはどのような挙動になるのかを確認していきます。

Using the Amazon Redshift Data API - Amazon Redshift

正常系の流れ

まず正常時のAPIの挙動を確認して行きます。今回はboto3を使用して、LambdaからData APIを叩いていきます。

redshift-data — AWS CLI 1.19.44 Command Reference

1ノード構成のdc2.largeで、「パブリックアクセス無効」「セキュリティグループは全クローズ」にしたプライベートなクラスタに対して実行してみます。LambdaからDataAPIを叩くサンプルは、以下のブログに修正を加えて流用しています。

import json
import boto3
import time

# Redshift接続情報
CLUSTER_NAME='haruta-data-api'
DATABASE_NAME='dev'
DB_USER='awsuser'
client = boto3.client('redshift-data')


def lambda_handler(event, context):
    sql = event['sql']
    exec_query(sql)


def exec_query(sql):
    result = client.execute_statement(
        ClusterIdentifier=CLUSTER_NAME,
        Database=DATABASE_NAME,
        DbUser=DB_USER,
        Sql=sql,
    )
    
    # 実行IDを取得
    id = result['Id']

    # クエリが終わるのを待つ
    statement = ''
    status = ''
    while status != 'FINISHED' and status != 'FAILED' and status != 'ABORTED':
        statement = client.describe_statement(Id=id)
        status = statement['Status']
        print("Status:", status)
        time.sleep(1)
    print('')
    print(json.dumps(statement, indent=4, default=str))
    print('')
    
    # 結果の表示
    try:
        statement = client.get_statement_result(Id=id)
        print(json.dumps(statement, indent=4, default=str))
    except Exception as e:
        print(e)

boto3(ver1.17.45)で用意されているAPIメソッドは以下の通りです。クエリを投げて結果を得る基本操作には、execute_statementdescribe_statementget_statement_resultを順次実行します。

まずはCREATE TABLE ASを実行してみました。結果を持たないクエリなので、get_statement_resultではResourceNotFoundExceptionが返ってきていますね。

create table hoge as select 1 as id, 'Taro' as name union select 2 as id, 'Jiro' as name;
Status: PICKED
Status: FINISHED
{
    "ClusterIdentifier": "haruta-data-api",
    "CreatedAt": "2021-04-06 05:42:42.925000+00:00",
    "Duration": 48020206,
    "Id": "69810e81-c3ae-4ac2-a64b-fc597a015e44",
    "QueryString": "drop table hoge;",
    "RedshiftPid": 7831,
    "RedshiftQueryId": -1,
    "ResultRows": 0,
    "ResultSize": 0,
    "Status": "FINISHED",
    "UpdatedAt": "2021-04-06 05:42:43.697000+00:00",
    "ResponseMetadata": {
        "RequestId": "4cdf734a-76f7-4564-8064-e584c2ed24d4",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
            "x-amzn-requestid": "4cdf734a-76f7-4564-8064-e584c2ed24d4",
            "content-type": "application/x-amz-json-1.1",
            "content-length": "305",
            "date": "Tue, 06 Apr 2021 05:42:44 GMT"
        },
        "RetryAttempts": 0
    }
}
An error occurred (ResourceNotFoundException) when calling the GetStatementResult operation: Query does not have result. Please check query status with DescribeStatement.

続いて作成したテーブルに対して、SELECT文を試してみます。当たり前ですが、こちらは正常に取得できました。

select * from hoge;
Status: SUBMITTED
Status: FINISHED

{
    "ClusterIdentifier": "haruta-data-api",
    "CreatedAt": "2021-04-06 05:56:09.907000+00:00",
    "Duration": 192198311,
    "Id": "c3f7d0ff-d243-4708-80d5-2f790f3f0b3d",
    "QueryString": "select * from hoge;",
    "RedshiftPid": 9675,
    "RedshiftQueryId": 2475,
    "ResultRows": 2,
    "ResultSize": 30,
    "Status": "FINISHED",
    "UpdatedAt": "2021-04-06 05:56:10.748000+00:00",
    "ResponseMetadata": {
        "RequestId": "c186a8e6-073e-4b35-9295-c13a7e2a9850",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
            "x-amzn-requestid": "c186a8e6-073e-4b35-9295-c13a7e2a9850",
            "content-type": "application/x-amz-json-1.1",
            "content-length": "311",
            "date": "Tue, 06 Apr 2021 05:56:11 GMT"
        },
        "RetryAttempts": 0
    }
}

{
    "ColumnMetadata": [
        {
            "isCaseSensitive": false,
            "isCurrency": false,
            "isSigned": true,
            "label": "id",
            "length": 0,
            "name": "id",
            "nullable": 1,
            "precision": 10,
            "scale": 0,
            "schemaName": "public",
            "tableName": "hoge",
            "typeName": "int4"
        },
        {
            "isCaseSensitive": true,
            "isCurrency": false,
            "isSigned": false,
            "label": "name",
            "length": 0,
            "name": "name",
            "nullable": 1,
            "precision": 4,
            "scale": 0,
            "schemaName": "public",
            "tableName": "hoge",
            "typeName": "varchar"
        }
    ],
    "Records": [
        [
            {
                "longValue": 1
            },
            {
                "stringValue": "Taro"
            }
        ],
        [
            {
                "longValue": 2
            },
            {
                "stringValue": "Jiro"
            }
        ]
    ],
    "TotalNumRows": 2,
    "ResponseMetadata": {
        "RequestId": "96465921-d6c3-4e25-ab26-031cf4e7a134",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
            "x-amzn-requestid": "96465921-d6c3-4e25-ab26-031cf4e7a134",
            "content-type": "application/x-amz-json-1.1",
            "content-length": "525",
            "date": "Tue, 06 Apr 2021 05:56:12 GMT"
        },
        "RetryAttempts": 0
    }
}

一通り正常系の挙動が見れたところで、異常系の挙動を確認して行きます。

異常時の挙動

テーブルの中身が0件

SQLで参照しているテーブルの中身がなかったケースです。エラーにはならず、Records内が空のリストとなってレスポンスが返ってきました。

create table null_table (id integer, name varchar);
select * from null_table;
Status: SUBMITTED
Status: STARTED
Status: FINISHED

{
    "ClusterIdentifier": "haruta-data-api",
    "CreatedAt": "2021-04-06 06:22:12.597000+00:00",
    "Duration": 1363452202,
    "Id": "162128c8-2153-49e6-9a57-ae5f3051a682",
    "QueryString": "select * from null_table;",
    "RedshiftPid": 14180,
    "RedshiftQueryId": 2840,
    "ResultRows": 0,
    "ResultSize": 0,
    "Status": "FINISHED",
    "UpdatedAt": "2021-04-06 06:22:14.895000+00:00",
    "ResponseMetadata": {
        "RequestId": "d3fbddaf-c996-4a66-827e-927b1400c5c5",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
            "x-amzn-requestid": "d3fbddaf-c996-4a66-827e-927b1400c5c5",
            "content-type": "application/x-amz-json-1.1",
            "content-length": "318",
            "date": "Tue, 06 Apr 2021 06:22:15 GMT"
        },
        "RetryAttempts": 0
    }
}

{
    "ColumnMetadata": [
        {
            "isCaseSensitive": false,
            "isCurrency": false,
            "isSigned": true,
            "label": "id",
            "length": 0,
            "name": "id",
            "nullable": 1,
            "precision": 10,
            "scale": 0,
            "schemaName": "public",
            "tableName": "null_table",
            "typeName": "int4"
        },
        {
            "isCaseSensitive": true,
            "isCurrency": false,
            "isSigned": false,
            "label": "name",
            "length": 0,
            "name": "name",
            "nullable": 1,
            "precision": 256,
            "scale": 0,
            "schemaName": "public",
            "tableName": "null_table",
            "typeName": "varchar"
        }
    ],
    "Records": [],
    "TotalNumRows": 0,
    "ResponseMetadata": {
        "RequestId": "17dbfff7-f1e4-4e94-bb2e-4d06d585558f",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
            "x-amzn-requestid": "17dbfff7-f1e4-4e94-bb2e-4d06d585558f",
            "content-type": "application/x-amz-json-1.1",
            "content-length": "458",
            "date": "Tue, 06 Apr 2021 06:22:16 GMT"
        },
        "RetryAttempts": 0
    }
}

テーブルが存在しない場合

SQLで参照しているテーブルがそもそも存在していなかったケースです。この場合describe_statementで、"Status": "FAILED""Error": "ERROR: relation "not_exist_table" does not exist"という情報が含まれたレスポンスが返され、プログラム自体は正常終了します。エラー終了させたい場合は、Statusから条件分岐させてraiseする必要があります。

一方、get_statement_resultではResourceNotFoundExceptionエラーが返ってきています。あくまでクエリ結果がないというだけの情報なので、詳細を把握するためにはdescribe_statementのレスポンスが必要です。

select * from not_exist_table;
Status: SUBMITTED
Status: FAILED

{
    "ClusterIdentifier": "haruta-data-api",
    "CreatedAt": "2021-04-06 06:24:42.729000+00:00",
    "Duration": -1,
    "Error": "ERROR: relation \"not_exist_table\" does not exist",
    "Id": "197892f7-febb-4ab1-9e63-2b13cf42e158",
    "QueryString": "select * from not_exist_table;",
    "RedshiftPid": 14966,
    "RedshiftQueryId": -1,
    "ResultRows": -1,
    "ResultSize": -1,
    "Status": "FAILED",
    "UpdatedAt": "2021-04-06 06:24:43.572000+00:00",
    "ResponseMetadata": {
        "RequestId": "dff3d4bc-c078-4cc3-a9a5-6f72c3050b6f",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
            "x-amzn-requestid": "dff3d4bc-c078-4cc3-a9a5-6f72c3050b6f",
            "content-type": "application/x-amz-json-1.1",
            "content-length": "375",
            "date": "Tue, 06 Apr 2021 06:24:44 GMT"
        },
        "RetryAttempts": 0
    }
}

An error occurred (ResourceNotFoundException) when calling the GetStatementResult operation: Query does not have result. Please check query status with DescribeStatement.

SQLのシンタックスエラー

シンタックスエラーのSQLが実行されたケースでは、テーブルが存在しない場合と同じようにPython自体は正常終了し、describe_statementの方でエラー情報が含まれたレスポンスが返ってきます。

elect * from hoge;
Status: SUBMITTED
Status: FAILED

{
    "ClusterIdentifier": "haruta-data-api",
    "CreatedAt": "2021-04-06 06:32:17.668000+00:00",
    "Duration": -1,
    "Error": "ERROR: syntax error at or near \"elect\"\n  Position: 1",
    "Id": "bb48e994-1543-4835-b158-1a96f3e1d800",
    "QueryString": "elect * from hoge;",
    "RedshiftPid": 15677,
    "RedshiftQueryId": -1,
    "ResultRows": -1,
    "ResultSize": -1,
    "Status": "FAILED",
    "UpdatedAt": "2021-04-06 06:32:18.488000+00:00",
    "ResponseMetadata": {
        "RequestId": "742ba7b6-3c3a-44c5-85e7-10a62896897e",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
            "x-amzn-requestid": "742ba7b6-3c3a-44c5-85e7-10a62896897e",
            "content-type": "application/x-amz-json-1.1",
            "content-length": "368",
            "date": "Tue, 06 Apr 2021 06:32:19 GMT"
        },
        "RetryAttempts": 0
    }
}

An error occurred (ResourceNotFoundException) when calling the GetStatementResult operation: Query does not have result. Please check query status with DescribeStatement.

パラメータ間違え

Redshiftのパラメータ情報が間違っていた場合の挙動です。CLUSTER_NAMEが間違っている場合はexecute_statement自体がエラー終了となりましたが、DATABASE_NAMEDB_USERが間違っている場合、describe_statementのレスポンス内にエラー情報が含まれ、プログラム自体は正常終了します。クラスタが存在していれば、APIサービス自体はリクエストを受け入れる挙動になってるんですねー。

CLUSTER_NAME

# Redshift接続情報
CLUSTER_NAME='haruta-data-api-dummy'
DATABASE_NAME='dev'
DB_USER='awsuser'
{
  "errorMessage": "An error occurred (ValidationException) when calling the ExecuteStatement operation: Cluster doesn't exist in this region.",
  "errorType": "ValidationException",
  "stackTrace": [
    "  File \"/var/task/lambda_function.py\", line 14, in lambda_handler\n    exec_query(sql)\n",
    "  File \"/var/task/lambda_function.py\", line 22, in exec_query\n    Sql=sql,\n",
    "  File \"/var/runtime/botocore/client.py\", line 357, in _api_call\n    return self._make_api_call(operation_name, kwargs)\n",
    "  File \"/var/runtime/botocore/client.py\", line 676, in _make_api_call\n    raise error_class(parsed_response, operation_name)\n"
  ]
}

DATABASE_NAME

# Redshift接続情報
CLUSTER_NAME='haruta-data-api'
DATABASE_NAME='dev-dummy'
DB_USER='awsuser'
Status: SUBMITTED
Status: FAILED

{
    "ClusterIdentifier": "haruta-data-api",
    "CreatedAt": "2021-04-06 06:43:31.474000+00:00",
    "Duration": -1,
    "Error": "FATAL: database \"dev-dummy\" does not exist",
    "Id": "b4178d6a-3bd5-4b7b-a0ac-db7ad9fb947f",
    "QueryString": "select * from hoge;",
    "RedshiftPid": 0,
    "RedshiftQueryId": -1,
    "ResultRows": -1,
    "ResultSize": -1,
    "Status": "FAILED",
    "UpdatedAt": "2021-04-06 06:43:32.058000+00:00",
    "ResponseMetadata": {
        "RequestId": "04d365c3-5e04-4ef1-91ee-6c436e18b305",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
            "x-amzn-requestid": "04d365c3-5e04-4ef1-91ee-6c436e18b305",
            "content-type": "application/x-amz-json-1.1",
            "content-length": "354",
            "date": "Tue, 06 Apr 2021 06:43:33 GMT"
        },
        "RetryAttempts": 0
    }
}

An error occurred (ResourceNotFoundException) when calling the GetStatementResult operation: Query does not have result. Please check query status with DescribeStatement.

DB_USER

# Redshift接続情報
CLUSTER_NAME='haruta-data-api'
DATABASE_NAME='dev'
DB_USER='awsuser-dummy'
Status: SUBMITTED
Status: FAILED

{
    "ClusterIdentifier": "haruta-data-api",
    "CreatedAt": "2021-04-06 06:44:56.492000+00:00",
    "Duration": -1,
    "Error": "FATAL: user \"awsuser-dummy\" does not exist",
    "Id": "34053a23-9db4-4846-a123-06afe6e85dd0",
    "QueryString": "select * from hoge;",
    "RedshiftPid": 0,
    "RedshiftQueryId": -1,
    "ResultRows": -1,
    "ResultSize": -1,
    "Status": "FAILED",
    "UpdatedAt": "2021-04-06 06:44:57.194000+00:00",
    "ResponseMetadata": {
        "RequestId": "d29f0661-57a1-4082-80b3-51b83d15fa33",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
            "x-amzn-requestid": "d29f0661-57a1-4082-80b3-51b83d15fa33",
            "content-type": "application/x-amz-json-1.1",
            "content-length": "354",
            "date": "Tue, 06 Apr 2021 06:44:58 GMT"
        },
        "RetryAttempts": 0
    }
}

An error occurred (ResourceNotFoundException) when calling the GetStatementResult operation: Query does not have result. Please check query status with DescribeStatement.

パラメータ不足

execute_statementで必要なパラメータが指定されていない場合では、以下の例では全てPython上のエラー終了となりました。boto3のドキュメント上でREQUIREDとなっているものはMissing required parameter in inputというエラーとなり、それ以外は個別のエラーメッセージが返されています。REQUIRED以外のパラメータは、DataAPIを叩く認証設定にもよりそうですね。

ClusterIdentifier

    result = client.execute_statement(
        #ClusterIdentifier=CLUSTER_NAME,
        Database=DATABASE_NAME,
        DbUser=DB_USER,
        Sql=sql,
    )
{
  "errorMessage": "Parameter validation failed:\nMissing required parameter in input: \"ClusterIdentifier\"",
  "errorType": "ParamValidationError",
  "stackTrace": [
    "  File \"/var/task/lambda_function.py\", line 15, in lambda_handler\n    exec_query(sql)\n",
    "  File \"/var/task/lambda_function.py\", line 23, in exec_query\n    Sql=sql,\n",
    "  File \"/var/runtime/botocore/client.py\", line 357, in _api_call\n    return self._make_api_call(operation_name, kwargs)\n",
    "  File \"/var/runtime/botocore/client.py\", line 649, in _make_api_call\n    api_params, operation_model, context=request_context)\n",
    "  File \"/var/runtime/botocore/client.py\", line 697, in _convert_to_request_dict\n    api_params, operation_model)\n",
    "  File \"/var/runtime/botocore/validate.py\", line 297, in serialize_to_request\n    raise ParamValidationError(report=report.generate_report())\n"
  ]
}

Database

    result = client.execute_statement(
        ClusterIdentifier=CLUSTER_NAME,
        #Database=DATABASE_NAME,
        DbUser=DB_USER,
        Sql=sql,
    )
{
  "errorMessage": "An error occurred (ValidationException) when calling the ExecuteStatement operation: Database and SQL String are required attributes.",
  "errorType": "ValidationException",
  "stackTrace": [
    "  File \"/var/task/lambda_function.py\", line 15, in lambda_handler\n    exec_query(sql)\n",
    "  File \"/var/task/lambda_function.py\", line 23, in exec_query\n    Sql=sql,\n",
    "  File \"/var/runtime/botocore/client.py\", line 357, in _api_call\n    return self._make_api_call(operation_name, kwargs)\n",
    "  File \"/var/runtime/botocore/client.py\", line 676, in _make_api_call\n    raise error_class(parsed_response, operation_name)\n"
  ]
}

DbUser

    result = client.execute_statement(
        ClusterIdentifier=CLUSTER_NAME,
        Database=DATABASE_NAME,
        #DbUser=DB_USER,
        Sql=sql,
    )
{
  "errorMessage": "An error occurred (ValidationException) when calling the ExecuteStatement operation: To use IAM Authorization both Cluster ID and DB User have to specified.",
  "errorType": "ValidationException",
  "stackTrace": [
    "  File \"/var/task/lambda_function.py\", line 15, in lambda_handler\n    exec_query(sql)\n",
    "  File \"/var/task/lambda_function.py\", line 23, in exec_query\n    Sql=sql,\n",
    "  File \"/var/runtime/botocore/client.py\", line 357, in _api_call\n    return self._make_api_call(operation_name, kwargs)\n",
    "  File \"/var/runtime/botocore/client.py\", line 676, in _make_api_call\n    raise error_class(parsed_response, operation_name)\n"
  ]
}

Sql

    result = client.execute_statement(
        ClusterIdentifier=CLUSTER_NAME,
        Database=DATABASE_NAME,
        DbUser=DB_USER,
        #Sql=sql,
    )
{
  "errorMessage": "An error occurred (ValidationException) when calling the ExecuteStatement operation: To use IAM Authorization both Cluster ID and DB User have to specified.",
  "errorType": "ValidationException",
  "stackTrace": [
    "  File \"/var/task/lambda_function.py\", line 15, in lambda_handler\n    exec_query(sql)\n",
    "  File \"/var/task/lambda_function.py\", line 23, in exec_query\n    Sql=sql,\n",
    "  File \"/var/runtime/botocore/client.py\", line 357, in _api_call\n    return self._make_api_call(operation_name, kwargs)\n",
    "  File \"/var/runtime/botocore/client.py\", line 676, in _make_api_call\n    raise error_class(parsed_response, operation_name)\n"
  ]
}

クラスタが一時停止中

最後に、クラスタが一時停止中の場合です。これもPython上のエラーとなりました。

{
  "errorMessage": "An error occurred (ValidationException) when calling the ExecuteStatement operation: Redshift cluster status 'PAUSED' is not allowed.",
  "errorType": "ValidationException",
  "stackTrace": [
    "  File \"/var/task/lambda_function.py\", line 15, in lambda_handler\n    exec_query(sql)\n",
    "  File \"/var/task/lambda_function.py\", line 23, in exec_query\n    Sql=sql,\n",
    "  File \"/var/runtime/botocore/client.py\", line 357, in _api_call\n    return self._make_api_call(operation_name, kwargs)\n",
    "  File \"/var/runtime/botocore/client.py\", line 676, in _make_api_call\n    raise error_class(parsed_response, operation_name)\n"
  ]
}

最後に

「パラメータ間違え」のパターンだけ、パラメータによって挙動が変わってくることだけ頭に入れておけば大丈夫かなと思います。Redshift Data APIのエラーハンドリングにお役てください!