CLOVER🍀

That was when it all began.

AWS SDK for Java 2.xでAPI呼び出しの結果がエラーの場合、例外がスローされるのか?(SdkHttpResponseのisSuccessfulは?)

これは、なにをしたくて書いたもの?

AWS SDK for Java 2.xを使うと、APIの各種レスポンスからSdkHttpResponseというインターフェースのインスタンスを取得することが
できます。

こちらですね。

SdkHttpResponse (AWS SDK for Java - 2.20.17)

このクラスにはisSuccessfulという成功/失敗を判定できそうなメソッドがあります。

If we get back any 2xx status code, then we know we should treat the service call as successful.

APIの呼び出し結果を扱う時に、エラーハンドリングとしてこの値を見た方がいいのかどうなのかよくわからなかったので、ちょっと
調べてみることにしました。

Amazon S3を例に

Amazon S3を例に、ちょっと見てみましょう。

Amazon S3にアクセスする際には、S3Clientというインターフェース(またはS3AsyncClient)を使います。

S3Client (AWS SDK for Java - 2.20.17)

バケットを作成するcreateBucketを見てみると、問題があった時には以下のように例外がスローされるであろうことがわかります。

default CreateBucketResponse createBucket(CreateBucketRequest createBucketRequest)
                                   throws BucketAlreadyExistsException,
                                          BucketAlreadyOwnedByYouException,
                                          AwsServiceException,
                                          SdkClientException,
                                          S3Exception

一方で、その戻り値であるCreateBucketResponseの定義を見ると、sdkHttpResponseというメソッドがあり。

CreateBucketResponse (AWS SDK for Java - 2.20.17)

こちらから先述のSdkHttpResponseを得ることができ、isSuccessfulというメソッドを備えています。

これを見ていると、スローされる例外とは別にSdkHttpResponse#isSuccessfulの結果も見ないといけないのでは?と思ってしまったのが
今回のエントリーを書いた理由ですね。

ドキュメントはどう言っているか?

ここで、AWS SDK for Java 2.xの例外処理のドキュメントを見てみましょう。

AWS SDK for Java 2.x の例外処理 - AWS SDK for Java 2.x

AwsServiceExceptionおよびそのサブクラスに関する記述を見ると、API呼び出し時にエラーが発生した時はAwsServiceExceptionの
インスタンス(およびそのサブクラス含む)がスローされることが書かれています。

AWS SDK for Java 2.x の例外処理 / AwsServiceException (およびサブクラス)

たとえば、最初に書いたS3Client#createBucketメソッドからスローされるとされているBucketAlreadyOwnedByYouExceptionクラスも、
AwsServiceExceptionのサブクラスですね。

BucketAlreadyOwnedByYouException (AWS SDK for Java - 2.20.17)

例外の詳細については、AwsErrorDetailsクラスを確認すればよい、となっています。

AwsErrorDetails (AWS SDK for Java - 2.20.17)

こちらのerrorCodeから、各サービスのドキュメントを確認することができます。たとえば、Amazon S3の場合はこちら。

Error Responses / List of Error Codes

AwsErrorDetailsのインスタンスは、AwsServiceExceptionから取得できます。

AwsServiceException (AWS SDK for Java - 2.20.17)

また、そもそもAPI呼び出しができないような状況の時にはSdkClientExceptionがスローされる、とも書かれています。

AWS SDK for Java 2.x の例外処理 / SdkClientException

こちらですね。

SdkClientException (AWS SDK for Java - 2.20.17)

どちらにせよ、SdkHttpResponseインターフェースはまったく登場しません。

このことから、APIの呼び出しに失敗すると例外がスローされるので、API呼び出しの成功確認にSdkHttpResponse#isSuccessfulメソッドを
参照しなくても良さそうな気がしますね。

今回、こちらをもう少し追ってみましょう。LocalStackとAWS SDK for Java 2.xのS3Clientを使って試してみたいと思います。

環境

今回の環境は、こちら。

LocalStack。

$ python3 -V
Python 3.10.6


$ localstack --version
1.4.0

起動。

$ localstack start

Javaまわり。

$ java --version
openjdk 17.0.6 2023-01-17
OpenJDK Runtime Environment (build 17.0.6+10-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 17.0.6+10-Ubuntu-0ubuntu122.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.0 (9b58d2bad23a66be161c4664ef21ce219c2c8584)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.6, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-67-generic", arch: "amd64", family: "unix"

準備

Maven依存関係などは、こちら。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>software.amazon.awssdk</groupId>
                <artifactId>bom</artifactId>
                <version>2.20.17</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>s3</artifactId>
        </dependency>
    </dependencies>

サンプルプログラムを書く

今回用意したプログラムは、こちら。

src/main/java/org/littlewings/aws/App.java

package org.littlewings.aws;

import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode;
import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
import software.amazon.awssdk.awscore.exception.AwsServiceException;
import software.amazon.awssdk.http.HttpStatusFamily;
import software.amazon.awssdk.http.SdkHttpResponse;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
import software.amazon.awssdk.services.s3.model.CreateBucketResponse;

import java.net.URI;

public class App {
    public static void main(String... args) {
        try (S3Client s3Client = S3Client
                                    .builder()
                                    .credentialsProvider(
                                            StaticCredentialsProvider.create(AwsBasicCredentials.create("access_key_id", "secret_access_key"))
                                    )
                                    .region(Region.AP_EAST_1)
                                    .defaultsMode(DefaultsMode.AUTO)
                                    .endpointOverride(URI.create("http://localhost:4566"))
                                    .forcePathStyle(true)
                                    .build()) {

            CreateBucketRequest request =
                    CreateBucketRequest
                            .builder()
                            .bucket("my-bucket")
                            .build();

            CreateBucketResponse response = s3Client.createBucket(request);

            SdkHttpResponse httpResponse = response.sdkHttpResponse();
            System.out.printf("isSuccessful = %b%n", httpResponse.isSuccessful());
            System.out.printf("statusCode = %d%n", httpResponse.statusCode());
            System.out.printf("httpStatusFamily = %s%n", HttpStatusFamily.of(httpResponse.statusCode()));
        } catch (AwsServiceException e) {
            AwsErrorDetails errorDetails = e.awsErrorDetails();
            System.out.printf("serviceName = %s%n", errorDetails.serviceName());
            System.out.printf("errorMessage = %s%n", errorDetails.errorMessage());
            System.out.printf("errorCode = %s%n", errorDetails.errorCode());

            SdkHttpResponse httpResponse = errorDetails.sdkHttpResponse();
            System.out.printf("isSuccessful = %b%n", httpResponse.isSuccessful());
            System.out.printf("statusCode = %d%n", httpResponse.statusCode());
            System.out.printf("httpStatusFamily = %s%n", HttpStatusFamily.of(httpResponse.statusCode()));

            e.printStackTrace();
        }
    }
}

Amazon S3バケットを作成するだけのプログラムです。

            CreateBucketRequest request =
                    CreateBucketRequest
                            .builder()
                            .bucket("my-bucket")
                            .build();

            CreateBucketResponse response = s3Client.createBucket(request);

あとは、レスポンスに含まれるSdkHttpResponseの内容を見たり、

            SdkHttpResponse httpResponse = response.sdkHttpResponse();
            System.out.printf("isSuccessful = %b%n", httpResponse.isSuccessful());
            System.out.printf("statusCode = %d%n", httpResponse.statusCode());
            System.out.printf("httpStatusFamily = %s%n", HttpStatusFamily.of(httpResponse.statusCode()));

例外ハンドリングを行ったり。

        } catch (AwsServiceException e) {
            AwsErrorDetails errorDetails = e.awsErrorDetails();
            System.out.printf("serviceName = %s%n", errorDetails.serviceName());
            System.out.printf("errorMessage = %s%n", errorDetails.errorMessage());
            System.out.printf("errorCode = %s%n", errorDetails.errorCode());

            SdkHttpResponse httpResponse = errorDetails.sdkHttpResponse();
            System.out.printf("isSuccessful = %b%n", httpResponse.isSuccessful());
            System.out.printf("statusCode = %d%n", httpResponse.statusCode());
            System.out.printf("httpStatusFamily = %s%n", HttpStatusFamily.of(httpResponse.statusCode()));

            e.printStackTrace();
        }

スローされる例外はAwsServiceExceptionクラスのサブクラスなので、こちらで一気に捕捉することにします。

確認してみる

作成したプログラムを動かしてみましょう。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.aws.App

結果。

isSuccessful = true
statusCode = 200
httpStatusFamily = SUCCESSFUL

もう1度実行すると、すでにS3バケットは作成されているので例外がスローされます。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.aws.App

こんな感じですね。

serviceName = S3
errorMessage = Your previous request to create the named bucket succeeded and you already own it.
errorCode = BucketAlreadyOwnedByYou
isSuccessful = false
statusCode = 409
httpStatusFamily = CLIENT_ERROR
software.amazon.awssdk.services.s3.model.BucketAlreadyOwnedByYouException: Your previous request to create the named bucket succeeded and you already own it. (Service: S3, Status Code: 409, Request ID: 44425877V1D0A2F9, Extended Request ID: MzRISOwyjmnup2476E87BD8C5CA707/JypPGXLh0OVFGcJaaO3KW/hRAqKOpIEEp)
        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handleErrorResponse(AwsXmlPredicatedResponseHandler.java:156)
        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handleResponse(AwsXmlPredicatedResponseHandler.java:108)
        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handle(AwsXmlPredicatedResponseHandler.java:85)
        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handle(AwsXmlPredicatedResponseHandler.java:43)
        at software.amazon.awssdk.awscore.client.handler.AwsSyncClientHandler$Crc32ValidationResponseHandler.handle(AwsSyncClientHandler.java:95)
        at software.amazon.awssdk.core.internal.handler.BaseClientHandler.lambda$successTransformationResponseHandler$7(BaseClientHandler.java:270)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.HandleResponseStage.execute(HandleResponseStage.java:40)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.HandleResponseStage.execute(HandleResponseStage.java:30)
        at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:73)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:42)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:78)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:40)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptMetricCollectionStage.execute(ApiCallAttemptMetricCollectionStage.java:50)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptMetricCollectionStage.execute(ApiCallAttemptMetricCollectionStage.java:36)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.execute(RetryableStage.java:81)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.execute(RetryableStage.java:36)
        at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
        at software.amazon.awssdk.core.internal.http.StreamManagingStage.execute(StreamManagingStage.java:56)
        at software.amazon.awssdk.core.internal.http.StreamManagingStage.execute(StreamManagingStage.java:36)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.executeWithTimer(ApiCallTimeoutTrackingStage.java:80)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.execute(ApiCallTimeoutTrackingStage.java:60)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.execute(ApiCallTimeoutTrackingStage.java:42)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallMetricCollectionStage.execute(ApiCallMetricCollectionStage.java:48)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallMetricCollectionStage.execute(ApiCallMetricCollectionStage.java:31)
        at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
        at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ExecutionFailureExceptionReportingStage.execute(ExecutionFailureExceptionReportingStage.java:37)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ExecutionFailureExceptionReportingStage.execute(ExecutionFailureExceptionReportingStage.java:26)
        at software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient$RequestExecutionBuilderImpl.execute(AmazonSyncHttpClient.java:193)
        at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.invoke(BaseSyncClientHandler.java:103)
        at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.doExecute(BaseSyncClientHandler.java:171)
        at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.lambda$execute$1(BaseSyncClientHandler.java:82)
        at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.measureApiCallSuccess(BaseSyncClientHandler.java:179)
        at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.execute(BaseSyncClientHandler.java:76)
        at software.amazon.awssdk.core.client.handler.SdkSyncClientHandler.execute(SdkSyncClientHandler.java:45)
        at software.amazon.awssdk.awscore.client.handler.AwsSyncClientHandler.execute(AwsSyncClientHandler.java:56)
        at software.amazon.awssdk.services.s3.DefaultS3Client.createBucket(DefaultS3Client.java:1149)
        at org.littlewings.aws.App.main(App.java:36)
        at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)
        at java.base/java.lang.Thread.run(Thread.java:833)

今回はBucketAlreadyOwnedByYouExceptionがスローされました。

software.amazon.awssdk.services.s3.model.BucketAlreadyOwnedByYouException: Your previous request to create the named bucket succeeded and you already own it. (Service: S3, Status Code: 409, Request ID: 44425877V1D0A2F9, Extended Request ID: MzRISOwyjmnup2476E87BD8C5CA707/JypPGXLh0OVFGcJaaO3KW/hRAqKOpIEEp)

例外に含まれているAwsErrorDetailsから得られた情報はこちら。

serviceName = S3
errorMessage = Your previous request to create the named bucket succeeded and you already own it.
errorCode = BucketAlreadyOwnedByYou

AwsErrorDetailsから取得できる、SdkHttpResponseの情報はこちら。

isSuccessful = false
statusCode = 409
httpStatusFamily = CLIENT_ERROR

このケースではSdkHttpResponse#isSuccessfulがfalseになっています。そもそも、HTTPステータスコードが2xxの場合にtrueになる
プロパティですからね。

If we get back any 2xx status code, then we know we should treat the service call as successful.

というわけで、SdkHttpResponse#isSuccessfulがfalseになるケースではやはり例外がスローされる気がします。

実装は?

少し、実装を見てみましょう。S3Clientインターフェースの実装クラスを少し見てみます。

$ unzip -p $HOME/.m2/repository/software/amazon/awssdk/s3/2.20.17/s3-2.20.17-sources.jar software/amazon/awssdk/services/s3/DefaultS3Client.java

こんな感じで、エラーコードと例外がマッピングされている箇所がありました。

    private AwsS3ProtocolFactory init() {
        return AwsS3ProtocolFactory
                .builder()
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("NoSuchUpload")
                                .exceptionBuilderSupplier(NoSuchUploadException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("ObjectAlreadyInActiveTierError")
                                .exceptionBuilderSupplier(ObjectAlreadyInActiveTierErrorException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("BucketAlreadyExists")
                                .exceptionBuilderSupplier(BucketAlreadyExistsException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("NoSuchBucket")
                                .exceptionBuilderSupplier(NoSuchBucketException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("InvalidObjectState")
                                .exceptionBuilderSupplier(InvalidObjectStateException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("ObjectNotInActiveTierError")
                                .exceptionBuilderSupplier(ObjectNotInActiveTierErrorException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("BucketAlreadyOwnedByYou")
                                .exceptionBuilderSupplier(BucketAlreadyOwnedByYouException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("NoSuchKey").exceptionBuilderSupplier(NoSuchKeyException::builder)
                                .build()).clientConfiguration(clientConfiguration)
                .defaultServiceExceptionSupplier(S3Exception::builder).build();
    }

先ほどのサンプルアプリケーションでS3バケットの作成失敗時に、AwsErrorDetailsから得られたエラーコードはBucketAlreadyOwnedByYou
なので、こちらが該当しますね。

                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("BucketAlreadyOwnedByYou")
                                .exceptionBuilderSupplier(BucketAlreadyOwnedByYouException::builder).build())

マッピングされるエラーコードがない場合は、S3Exceptionがスローされるように見えます。

                .defaultServiceExceptionSupplier(S3Exception::builder).build();

こう見ると、S3Client#createBucketのthrowsに書かれている内容がわかってきますね。

default CreateBucketResponse createBucket(CreateBucketRequest createBucketRequest)
                                   throws BucketAlreadyExistsException,
                                          BucketAlreadyOwnedByYouException,
                                          AwsServiceException,
                                          SdkClientException,
                                          S3Exception

エラーコードがマッピングできた場合はBucketAlreadyExistsException、BucketAlreadyOwnedByYouException、それ以外は
S3Exception、AwsServiceException、そもそもAPI呼び出しができなかった場合はSdkClientExceptionという感じですね。

S3AsyncClientの実装も見てみましたが、同じ感じでした。

$ unzip -p $HOME/.m2/repository/software/amazon/awssdk/s3/2.20.17/s3-2.20.17-sources.jar software/amazon/awssdk/services/s3/DefaultS3AsyncClient.java

こちらですね。

    private AwsS3ProtocolFactory init() {
        return AwsS3ProtocolFactory
                .builder()
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("NoSuchUpload")
                                .exceptionBuilderSupplier(NoSuchUploadException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("ObjectAlreadyInActiveTierError")
                                .exceptionBuilderSupplier(ObjectAlreadyInActiveTierErrorException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("BucketAlreadyExists")
                                .exceptionBuilderSupplier(BucketAlreadyExistsException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("NoSuchBucket")
                                .exceptionBuilderSupplier(NoSuchBucketException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("InvalidObjectState")
                                .exceptionBuilderSupplier(InvalidObjectStateException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("ObjectNotInActiveTierError")
                                .exceptionBuilderSupplier(ObjectNotInActiveTierErrorException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("BucketAlreadyOwnedByYou")
                                .exceptionBuilderSupplier(BucketAlreadyOwnedByYouException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("NoSuchKey").exceptionBuilderSupplier(NoSuchKeyException::builder)
                                .build()).clientConfiguration(clientConfiguration)
                .defaultServiceExceptionSupplier(S3Exception::builder).build();
    }

ところで、AWS SDK for Java 2.xのGitHubリポジトリを見ても、これらのクラスは存在しません。

GitHub - aws/aws-sdk-java-v2: The official AWS SDK for Java - Version 2

リクエスト、レスポンスまわりのクラスの雰囲気からもなんとなく察しがつきますが、これらのクラスは自動生成な感じがしますね。

@Generated("software.amazon.awssdk:codegen")
@SdkInternalApi
final class DefaultS3Client implements S3Client {


...


@Generated("software.amazon.awssdk:codegen")
public final class CreateBucketResponse extends S3Response implements


...


@Generated("software.amazon.awssdk:codegen")
public final class BucketAlreadyOwnedByYouException extends S3Exception implements
        ToCopyableBuilder<BucketAlreadyOwnedByYouException.Builder, BucketAlreadyOwnedByYouException> {

ここでは、このあたりは追いませんが。

気にするのは、先ほどのスタックトレースで出現していたこのあたりかなと。

        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handleErrorResponse(AwsXmlPredicatedResponseHandler.java:156)
        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handleResponse(AwsXmlPredicatedResponseHandler.java:108)
        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handle(AwsXmlPredicatedResponseHandler.java:85)
        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handle(AwsXmlPredicatedResponseHandler.java:43)
        at software.amazon.awssdk.awscore.client.handler.AwsSyncClientHandler$Crc32ValidationResponseHandler.handle(AwsSyncClientHandler.java:95)
        at software.amazon.awssdk.core.internal.handler.BaseClientHandler.lambda$successTransformationResponseHandler$7(BaseClientHandler.java:270

BaseClientHandlerでレスポンスのハンドリングを行っている箇所は、こちら。API呼び出しが成功した場合は、Response内に
SdkHttpResponseなどが含まれることになります。

            Response<OutputT> delegateResponse = responseHandler.handle(response, executionAttributes);

https://github.com/aws/aws-sdk-java-v2/blob/2.20.17/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseClientHandler.java#L270

そして、APIの呼び出し結果が成功/失敗による分岐。

        if (parsedResponse.isResponseSuccess()) {
            OutputT response = handleSuccessResponse(parsedResponse);
            return Response.<OutputT>builder().httpResponse(httpResponse)
                                              .response(response)
                                              .isSuccess(true)
                                              .build();
        } else {
            return Response.<OutputT>builder().httpResponse(httpResponse)
                                              .exception(handleErrorResponse(parsedResponse))
                                              .isSuccess(false)
                                              .build();
        }

https://github.com/aws/aws-sdk-java-v2/blob/2.20.17/core/protocols/aws-xml-protocol/src/main/java/software/amazon/awssdk/protocols/xml/internal/unmarshall/AwsXmlPredicatedResponseHandler.java#L100-L111

handleErrorResponseメソッドでは、実際にスローされる例外が作成されます。SdkExceptionの部分ですね。

            SdkException exception = errorResponseTransformer.apply(parsedResponse);
            exception.fillInStackTrace();
            return exception;

https://github.com/aws/aws-sdk-java-v2/blob/2.20.17/core/protocols/aws-xml-protocol/src/main/java/software/amazon/awssdk/protocols/xml/internal/unmarshall/AwsXmlPredicatedResponseHandler.java#L155-L157

エラーコードから、例外に変換しているのは以下の箇所のようです。一致するものがなかった場合は、デフォルトの例外が選択される
ようです。

        AwsServiceException.Builder builder = errorRoot
            .map(e -> invokeSafely(() -> unmarshallFromErrorCode(response, e, errorCode)))
            .orElseGet(this::defaultException);

https://github.com/aws/aws-sdk-java-v2/blob/2.20.17/core/protocols/aws-query-protocol/src/main/java/software/amazon/awssdk/protocols/query/internal/unmarshall/AwsXmlErrorUnmarshaller.java#L80-L82

そして、この例外がスローされることになる、というわけですね。

あとは、先ほどの以下の分岐でtrue/falseになる情報を見ると良さそうです。

        if (parsedResponse.isResponseSuccess()) {
            OutputT response = handleSuccessResponse(parsedResponse);
            return Response.<OutputT>builder().httpResponse(httpResponse)
                                              .response(response)
                                              .isSuccess(true)
                                              .build();
        } else {
            return Response.<OutputT>builder().httpResponse(httpResponse)
                                              .exception(handleErrorResponse(parsedResponse))
                                              .isSuccess(false)
                                              .build();
        }

これは、以下の部分ですね。

        if (!context.sdkHttpFullResponse().isSuccessful()) {
            // Request was non-2xx, defer to protocol handler for error root
            Optional<XmlElement> parsedErrorXml = parsedRootXml.flatMap(errorRootLocationFunction);
            return context.toBuilder().isResponseSuccess(false).parsedErrorXml(parsedErrorXml.orElse(null)).build();
        }

https://github.com/aws/aws-sdk-java-v2/blob/2.20.17/core/protocols/aws-xml-protocol/src/main/java/software/amazon/awssdk/protocols/xml/internal/unmarshall/DecorateErrorFromResponseBodyUnmarshaller.java#L58-L62

ここでSdkHttpFullReponseというのは、SdkHttpResponseインターフェースを拡張したものです。

SdkHttpFullResponse (AWS SDK for Java - 2.20.17)

ということは、SdkHttpFullReponse#isSuccessfulがfalseの時に例外がスローされることになるので、正常にAPI呼び出しが完了した場合は
SdkHttpResponse#isSuccessfulはtrueになり、falseになるケースでは例外がスローされることになります。

なので、AWS SDK for Java 2.xの例外処理ではSdkHttpResponse#isSuccessfulの話は出てこない、となりそうです。

AWS SDK for Java 2.x の例外処理 - AWS SDK for Java 2.x

実行パスを全部見たかというとそういうわけでもないのですが、考え方としてはそんなに誤っていないと思うのですが…どうでしょう?

なお、API呼び出しが正常にできた場合にレスポンスのオブジェクトを見ないでいいかというと、レスポンスから得たい情報はAPI次第かと
思います(呼び出し結果の値に関心があるかどうか)。

まとめ

AWS SDK for Java 2.xのSdkHttpResponse#isSuccessfulが気になったので、ちょっと確認してみました。

例外がスローされるかどうかに加えて、SdkHttpResponse#isSuccessfulを見る必要があるかどうかで迷っていたのですが、これはどうやら
スルーして良さそうです。

レスポンスのオブジェクトからは、API呼び出しが正常に終了した場合に欲しい情報を取得するのに使う感じですね。

Ubuntu Linux 22.04 LTSにRabbitMQをインストールする

これは、なにをしたくて書いたもの?

久しぶりに、RabbitMQを使ってみたくなりまして。まずはインストールするところから見直してみたいと思います。

RabbitMQ

RabbitMQのWebサイトはこちら。

Messaging that just works — RabbitMQ

機能はOSS版、商用版それぞれ以下に書かれています。

ざっくりこんな感じでしょうか。

ドキュメントは、こちら。

Documentation: Table of Contents — RabbitMQ

RabbitMQの基本的な機能の使い方は、Getting Startedがとてもわかりやすくまとまっていると思います。

RabbitMQ Tutorials — RabbitMQ

以下が紹介されています。

  • キュー
  • キューを使った複数のワーカーへのタスク送信
  • パブリッシュ/サブスクライブ
  • ルーティング
  • トピック
  • RPC
  • 送信確認

その他、ドキュメント内にある以下あたりも気になるところです。

とりあえず、今回はRabbitMQをインストールしてみましょう。

環境

今回の環境は、こちらです。Ubuntu Linux 22.04 LTSを対象にします。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.2 LTS
Release:        22.04
Codename:       jammy


$ uname -srvmpio
Linux 5.15.0-60-generic #66-Ubuntu SMP Fri Jan 20 14:29:49 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

RabbitMQをインストールする

RabbitMQのダウンロードやインストールに関するページはこちら。

Downloading and Installing RabbitMQ — RabbitMQ

今回はUbuntu Linux 22.04 LTSにインストールするので、こちらのページを参照します。

Installing on Debian and Ubuntu — RabbitMQ

インストール方法は、次の3つがあるようです。

  • Cloudsmithのaptリポジトリーを使ってパッケージインストールする
  • PackageCloudとLaunchpadのaptリポジトリーを使ってパッケージインストールする
  • debパッケージを使ってインストールする

Installing on Debian and Ubuntu / How to Install Latest RabbitMQ on Debian and Ubuntu

推奨は、aptリポジトリーからのインストールです。

サポートされているUbuntu LinuxおよびDebianは、以下になります。

Installing on Debian and Ubuntu / / Supported Distributions

今回はPackageCloudとLaunchpadのaptリポジトリーを使うことにします。

Installing on Debian and Ubuntu / Using RabbitMQ Apt Repositories on PackageCloud

依存パッケージをインストール。

$ sudo apt-get install apt-transport-https

リポジトリーの署名キーを追加。

$ curl -1sLf "https://keys.openpgp.org/vks/v1/by-fingerprint/0A9AF2115F4687BD29803A206B73A36E6026DFCA" | sudo gpg --dearmor | sudo tee /usr/share/keyrings/com.rabbitmq.team.gpg > /dev/null
$ curl -1sLf "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xf77f1eda57ebb1cc" | sudo gpg --dearmor | sudo tee /usr/share/keyrings/net.launchpad.ppa.rabbitmq.erlang.gpg > /dev/null
$ curl -1sLf "https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey" | sudo gpg --dearmor | sudo tee /usr/share/keyrings/io.packagecloud.rabbitmq.gpg > /dev/null

sources.listへの追加。

$ sudo tee /etc/apt/sources.list.d/rabbitmq.list <<EOF
deb [signed-by=/usr/share/keyrings/net.launchpad.ppa.rabbitmq.erlang.gpg] http://ppa.launchpad.net/rabbitmq/rabbitmq-erlang/ubuntu $(lsb_release -cs) main
deb-src [signed-by=/usr/share/keyrings/net.launchpad.ppa.rabbitmq.erlang.gpg] http://ppa.launchpad.net/rabbitmq/rabbitmq-erlang/ubuntu $(lsb_release -cs) main
deb [signed-by=/usr/share/keyrings/io.packagecloud.rabbitmq.gpg] https://packagecloud.io/rabbitmq/rabbitmq-server/ubuntu/ $(lsb_release -cs) main
deb-src [signed-by=/usr/share/keyrings/io.packagecloud.rabbitmq.gpg] https://packagecloud.io/rabbitmq/rabbitmq-server/ubuntu/ $(lsb_release -cs) main
EOF

lsb_release -csを使いましたが、使っているディストリビューション名と合わせることが必要です。

更新。

$ sudo apt update

Erlangに関するパッケージのインストール。

$ sudo apt install erlang-base \
                   erlang-asn1 erlang-crypto erlang-eldap erlang-ftp erlang-inets \
                   erlang-mnesia erlang-os-mon erlang-parsetools erlang-public-key \
                   erlang-runtime-tools erlang-snmp erlang-ssl \
                   erlang-syntax-tools erlang-tftp erlang-tools erlang-xmerl

RabbitMQのインストール。

$ sudo apt install rabbitmq-server --fix-missing

RabbitMQ 3.11.9がインストールされました。

$ sudo rabbitmqctl version
3.11.9

バージョンを指定してインストールする場合は、こうですかね。

$ sudo apt install rabbitmq-server=3.11.9-1 --fix-missing

指定できるバージョンは、こちらを見るとよさそうです。

rabbitmq/rabbitmq-server - Packages · packagecloud

RabbitMQを起動する

では、インストールしたRabbitMQを起動してみましょう。

Installing on Debian and Ubuntu / Run RabbitMQ Server

と言いたいところですが、インストール時点ですでに起動済みだったりします。

$ systemctl status rabbitmq-server
● rabbitmq-server.service - RabbitMQ broker
     Loaded: loaded (/lib/systemd/system/rabbitmq-server.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2023-02-23 23:41:26 JST; 35s ago
   Main PID: 4655 (beam.smp)
      Tasks: 25 (limit: 2237)
     Memory: 96.8M
        CPU: 5.998s
     CGroup: /system.slice/rabbitmq-server.service
             ├─4655 /usr/lib/erlang/erts-13.1.5/bin/beam.smp -W w -MBas ageffcbf -MHas ageffcbf -MBlmbcs 512 -MHlmbcs 512 -MMmcs 30 -P 1048576 -t 5000000 -stbt db -zdbbl 128000>
             ├─4665 erl_child_setup 32768
             ├─4693 /usr/lib/erlang/erts-13.1.5/bin/epmd -daemon
             ├─4714 /usr/lib/erlang/erts-13.1.5/bin/inet_gethost 4
             ├─4715 /usr/lib/erlang/erts-13.1.5/bin/inet_gethost 4
             └─4758 /bin/sh -s rabbit_disk_monitor

停止。

$ sudo systemctl stop rabbitmq-server

起動。

$ sudo systemctl start rabbitmq-server

Installing on Debian and Ubuntu / Managing the Service

RabbitMQに関するポートはこちら。

  • 4369: epmd, a peer discovery service used by RabbitMQ nodes and CLI tools
  • 5672, 5671: used by AMQP 0-9-1 and 1.0 clients without and with TLS
  • 25672: used for inter-node and CLI tools communication (Erlang distribution server port) and is allocated from a dynamic range (limited to a single - port by default, computed as AMQP port + 20000). Unless external connections on these ports are really necessary (e.g. the cluster uses federation or CLI tools are used on machines outside the subnet), these ports should not be publicly exposed. See networking guide for details.
  • 35672-35682: used by CLI tools (Erlang distribution client ports) for communication with nodes and is allocated from a dynamic range (computed as server distribution port + 10000 through server distribution port + 10010). See networking guide for details.
  • 15672: HTTP API clients, management UI and rabbitmqadmin (only if the management plugin is enabled)
  • 61613, 61614: STOMP clients without and with TLS (only if the STOMP plugin is enabled)
  • 1883, 8883: MQTT clients without and with TLS, if the MQTT plugin is enabled
  • 15674: STOMP-over-WebSockets clients (only if the Web STOMP plugin is enabled)
  • 15675: MQTT-over-WebSockets clients (only if the Web MQTT plugin is enabled)
  • 15692: Prometheus metrics (only if the Prometheus plugin is enabled)

Installing on Debian and Ubuntu / Port Access

ログファイルは/var/log/rabbitmqディレクトリ内にあります。

$ sudo ls -l /var/log/rabbitmq
合計 84
-rw-r----- 1 rabbitmq adm 79319  2月 23 23:42 rabbit@ubuntu2204.log
-rw-r----- 1 rabbitmq adm     0  2月 23 23:28 rabbit@ubuntu2204_upgrade.log

journalctlでも確認できます。

$ sudo journalctl -u rabbitmq-server

Installing on Debian and Ubuntu / Log Files and Management

設定について。

Installing on Debian and Ubuntu / Configuring RabbitMQ

こちらは、別ドキュメントになっています。

Configuration — RabbitMQ

設定ファイルの場所などは、こちらに記載があります。

Configuration / Configuration File(s)

今回のインストール方法だと、設定ファイルは作られないみたいですね。

$ sudo ls -l /etc/rabbitmq/
合計 0

データは/var/lib/rabbitmq/mnesiaディレクトリ内にありそうです。

ユーザーを作成する

インストール時には、guest/guestというlocalhostからのみ接続可能なユーザーが作成されるようです。

The broker creates a user guest with password guest. Unconfigured clients will in general use these credentials. By default, these credentials can only be used when connecting to the broker as localhost so you will need to take action before connecting from any other machine.

Installing on Debian and Ubuntu / Default User Access

ユーザーの追加など、ユーザーの管理についてはこちら。

Authentication, Authorisation, Access Control — RabbitMQ

まずは現在のユーザーを確認。

$ sudo rabbitmqctl list_users
Listing users ...
user    tags
guest   [administrator]

確かにguestというユーザーがありますね。

ユーザーはVirtual Hostへのアクセス許可を行うもののようです。

Different users can be granted access only to specific virtual hosts. Their permissions in each virtual hosts also can be limited.

デフォルトのVirtual Hostは、/のようですね。

a virtual host named / (a slash)

Authentication, Authorisation, Access Control / Default Virtual Host and User

Virtual Hostについては、こちら。

Virtual Hosts — RabbitMQ

RabbitMQはマルチテナントシステムであり、接続、Exchange、Queue、Binding、ユーザーの権限やポリシーなどはVirtual Hostという
論理的なグループで管理するようです。

RabbitMQ is multi-tenant system: connections, exchanges, queues, bindings, user permissions, policies and some other things belong to virtual hosts, logical groups of entities.

Virtual Hostは設定ファイルではなく、rabbitmqctlコマンドやHTTP APIで行います。

There is, however, one important difference: virtual hosts in Apache are defined in the configuration file; that's not the case with RabbitMQ: virtual hosts are created and deleted using rabbitmqctl or HTTP API instead.

話を戻して、ユーザーを作成しましょう。

Authentication, Authorisation, Access Control / Managing Users and Permissions

ユーザー名kazuhira、パスワードpasswordでユーザーを作成。

$ sudo -u rabbitmq rabbitmqctl add_user 'kazuhira' 'password'
Adding user "kazuhira" ...
Done. Don't forget to grant the user permissions to some virtual hosts! See 'rabbitmqctl help set_permissions' to learn more.

パスワードを指定する方法はいくつかあるようですが、今回は引数にそのまま指定しました。

ユーザーが作成されていることを確認。

$ sudo -u rabbitmq rabbitmqctl list_users
Listing users ...
user    tags
guest   [administrator]
kazuhira        []

次に、権限を与えます。

$ sudo -u rabbitmq rabbitmqctl set_permissions -p '/' 'kazuhira' '.*' '.*' '.*'
Setting permissions for user "kazuhira" in vhost "/" ...

Virtual Hostは/に、あとは全権限を与えました。

3つの引数の権限は、それぞれ以下のグルーピングのようです。

  • First ".*" for configure permission on every entity(設定)
  • Second ".*" for write permission on every entity(書き込み)
  • Third ".*" for read permission on every entity(読み取り)

確認。

$ sudo -u rabbitmq rabbitmqctl list_user_permissions kazuhira
Listing permissions for user "kazuhira" ...
vhost   configure       write   read
/       .*      .*      .*

これで、ユーザーが作成できました。

管理UIをインストールする

最後に、管理UIをインストールしましょう。

Management Plugin — RabbitMQ

RabbitMQには、組み込みの管理UIやHTTP APIによるモニタリングのオプションがあります。

Management Plugin / Management UI and External Monitoring Systems

管理UIにはいくつか制限があるので、本格的に運用する場合はPrometheusやGrafana、Elastic Stackの利用を勧めていますね。

今回は簡単に管理UIを使います。

Management Plugin / Getting Started

マネジメントプラグインの有効化。

$ sudo rabbitmq-plugins enable rabbitmq_management

即有効になり、特に再起動は不要みたいです。

Enabling plugins on node rabbit@ubuntu2204:
rabbitmq_management
The following plugins have been configured:
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_web_dispatch
Applying plugin configuration to rabbit@ubuntu2204...
The following plugins have been enabled:
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_web_dispatch

started 3 plugins.

ところで、このコマンドを実行するにはrootまたはrabbitmqユーザーで実行すること、と言われるのですが、sudo -u rabbitmqで
実行しても同じメッセージでエラーになりました…。

Only root or rabbitmq can run rabbitmq-plugins

http://[RabbitMQが稼働しているホスト]:15672/にアクセスすると、管理UIが確認できます。

先ほど作成したユーザーでログインしようとすると、「管理ユーザーではない」と言われます。

以下に従って、適切にタグを設定する必要がありそうです(権限ではなく)。

Management Plugin / Access and Permissions

例に従って、今回はadministratorタグを付与することにしましょう。

Management Plugin / Command Line Examples

$ sudo -u rabbitmq rabbitmqctl set_user_tags kazuhira administrator
Setting tags for user "kazuhira" to [administrator] ...

確認。

$ sudo -u rabbitmq rabbitmqctl list_users
Listing users ...
user    tags
guest   [administrator]
kazuhira        [administrator]

ユーザーに付与したタグは、rabbitmq list_usersで確認できるようです。

今度は管理UIにログインできるようになりました。

今回は、このくらいにしておきましょうか。

まとめ

久しぶりにRabbitMQをインストールしてみました。

前に試したのが7年くらい前なのでだいぶ変わっていましたが、特に困ることはなかったですね。
今回はドキュメントも眺めつつやったので、けっこう時間がかかりましたが。

これから少しずつ扱っていきましょう。