Scalaでfindify/s3mockによるテストを行おうとしてエラーにハマった結果max8github/s3mockでカバーした話

ScalaでS3Mockを使ったテストを試みたところ、モックサーバ側にトラブルが起きるという残念な状態になりました。解決方法についてまとめました。
2020.06.16

はじめに

ScalaでS3を利用した処理のテストを行いたい場合に、findify/s3mockを使う方法をよく見かけます。stackoverflowでもコード例を見かけるために非常に手間が省けるのですが、実際にやってみると環境によってはかなり頭を抱えるトラップがあります。

  • Bucketは作れる
  • putObjectはエラーになる

エラーの原因と、対処方法についてまとめました。

エラーの原因

なにがエラーで何がエラーじゃないのか、loggerを使って詳細を確認しました。その結果、

  • ファイル追加は正常に行われる
  • 追加した後の処理がエラーになる

という厄介な状態になっていることがわかりました。つまり、ファイルは置かれても例外によりassertがエラーになります。

原因としては、PutObject実行時にモックサーバがjdk.internal.ref.Cleanerによる500 Internal Server Errorを返すためでした。

DEBUG com.amazonaws.request - Received error response: com.amazonaws.services.s3.model.AmazonS3Exception: java.lang.IllegalAccessException: class org.iq80.leveldb.util.ByteBufferSupport cannot access class jdk.internal.ref.Cleaner (in module java.base) because module java.base does not export jdk.internal.ref to unnamed module @32a5e0f3 (Service: Amazon S3; Status Code: 500; Error Code: InternalError; Request ID: null; S3 Extended Request ID: null; Proxy: null), S3 Extended Request ID: null

例外を拾ってスルーする手もあるかもしれませんが、これ以外の例外に遭遇した時に詰みそうです。

Javaでの対処方法

結構致命的ですが、Javaを使っている場合には割と簡潔な対応策があるようでした。jvm-optsに以下の指定をいれましょう。

"--add-opens=java.base/jdk.internal.ref=ALL-UNNAMED"

build.gradleであれば以下の指定になります。

def jvmArgsAddOpens = [
"--add-opens=java.base/jdk.internal.ref=ALL-UNNAMED"
]

が、今回はScalaです。これが使えない。

Scalaでの対処方法

Issueでの調査結果によると、オプションによる調整手段は使えないようで、対策が取られているDockerイメージへの置き換えとなります。

The above happens when a putObject is issued. In FileProvider, the file gets saved, but not the metadata. The call metadataStore.put(bucket, key, objectMetadata) will cause an exception that will leave a file locked (a LOCK file in the metadata directory for the bucket). The offending code is in library leveldb, class DbLock. In its constructor, tryLock throws the exception above leaving metadata in an inconsistent state, thus compromising any successive getObject call (getObject will fail as it will find a lock).

All of that only happens with java 9, not with java 8. See https://bugs.openjdk.java.net/browse/JDK-8148117.

So for all that, the easy workaround is to either use java 8, or to use S3Mock's in memory provider (InMemoryProvider) instead of FileProvider.

Going to create a docker image tagging it with java 9 and "inmem". See https://hub.docker.com/r/max8github/s3mock/tags

$ docker run -p 8001:8001 -v /path/to/fixtures/:/tmp/s3mock/ -e "S3MOCK_DATA_DIR=/tmp/s3mock" max8github/s3mock:0.2.4.1-8-jdk-slim

なお、Issue内に記載されているタグは古いため機能しません。

まとめ

s3mockにはfindify/s3mockadobe/S3Mockの2つが存在します。adobe版は基本的にJavaでの利用が想定されているようで、JUnit4、JUnit5、TestNGが中心となります。そのためfindify版を選択しましたが、根が深いエラーに遭遇するという有様でした。

環境によって遭遇するかどうかも変わるかもしれませんが、同様のエラーに遭遇した場合に解消の一端となれば幸いです。

参考リンク