Jerseyで作成したRESTサービスをJUnitでテストする(Multipart)

ファイルアップロードなどMultipartでPOSTするRESTサービスのテストの書き方メモ

リソースクラスの例

package jp.yustam.jersey.resources;

import java.io.InputStream;

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import com.sun.jersey.multipart.FormDataParam;

@Path("file")
public class File{

    @POST
    @Consumes( { MediaType.MULTIPART_FORM_DATA })
    @Produces( { MediaType.TEXT_PLAIN })
    @Path("upload/{fileName}")
    public Response upload(@FormDataParam("file") InputStream stream,
            @PathParam("fileName") String fileName) {
        return Response.ok().entity("OK").build();
    }

}

テストクラスの例

package jp.yustam.jersey.resources;

import static org.junit.Assert.assertEquals;

import java.io.File;
import java.io.IOException;

import javax.ws.rs.core.MediaType;

import org.junit.Test;

import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.core.header.FormDataContentDisposition;
import com.sun.jersey.multipart.FormDataBodyPart;
import com.sun.jersey.multipart.FormDataMultiPart;
import com.sun.jersey.test.framework.JerseyTest;
import com.sun.jersey.test.framework.WebAppDescriptor;

public class FileTest extends JerseyTest {

    public FileTest() {
        super(new WebAppDescriptor.Builder("jp.yustam.jersey.resources").build());
    }

    @Test
    public void testUpload() throws IOException {
        File file = File.createTempFile("TMP", ".tmp");
        FormDataMultiPart form = new FormDataMultiPart();
        form.bodyPart(new FormDataBodyPart(FormDataContentDisposition
                .name("file").build(), file, MediaType.APPLICATION_OCTET_STREAM_TYPE));

        WebResource wr = resource().path("file/upload/fileName");
        ClientResponse response = wr.type(MediaType.MULTIPART_FORM_DATA)
                .post(ClientResponse.class, form);
        assertEquals(response.getEntity(String.class), "OK");

        file.delete();
    }

}

Jerseyで作成したRESTサービスをJUnitでテストする

Jersey Test Frameworkを使用する

Chapter 7. Jersey Test Framework
いくつか種類があるみたいですが今回使用したのは「jersey-test-framework-grizzly2」
pom.xmlに以下の依存関係を追加します。

<dependency>
  <groupId>com.sun.jersey.jersey-test-framework</groupId>
  <artifactId>jersey-test-framework-grizzly2</artifactId>
  <version>1.14</version>
  <scope>test</scope>
</dependency>

Testクラスを書く

web.xmlでパスを切っていたのですがテストクラスではweb.xmlの指定は必要ないみたいです。
テストクラス作成時のWebResourceのパス指定にて「services/sample/login/id/pw」でなく
sample/login/id/pw」となるので注意

リソースクラスの例

package jp.yustam.jersey.resources;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("sample")
public class MyService {

    @GET
    @Path("/login/{id}/{pw}")
    @Produces( { MediaType.TEXT_PLAIN })
    public Response login(@PathParam("id") String id, @PathParam("pw") String pw) {
        return Response.ok().entity("OK").build();
    }

}

web.xmlの例

<servlet>
    <servlet-name>MyRESTService</servlet-name>
    <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
    <init-param>
        <param-name>com.sun.jersey.config.property.resourceConfigClass</param-name>
        <param-value>com.sun.jersey.api.core.PackagesResourceConfig</param-value>
    </init-param>
    <init-param>
        <param-name>com.sun.jersey.config.property.packages</param-name>
        <param-value>jp.yustam.jersey.resources</param-value>
    </init-param>
    <init-param>
        <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
        <param-value>true</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
  <servlet-name>MyRESTService</servlet-name>
  <url-pattern>/services/*</url-pattern>
</servlet-mapping>

テストクラスの例

package jp.yustam.jersey.resources;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.test.framework.JerseyTest;
import com.sun.jersey.test.framework.WebAppDescriptor;

public class MyServiceTest extends JerseyTest {

    public MyServiceTest() {
        super(new WebAppDescriptor.Builder("jp.yustam.jersey.resources").build());
    }

    @Test
    public void testLogin() {
        WebResource wr = resource().path("sample/login/id/pw");
        ClientResponse response = wr.get(ClientResponse.class);
        assertEquals(response.getEntity(String.class), "OK");
    }

}

Amazon ElasticMapreduceメモ

Amazon ElasticMapreduceのテストを兼ねて性能測定を行ったので分かったことをメモ

ジョブフローの起動

ジョブフローを登録してからステータスが「RUNNING」に変わるまで4分~6分かかる
立ち上げるインスタンスの数が変わってもこの時間は変わらない

MapTaskの数

EMRに限らずHadoopの動作ですがインスタンス数などの起動設定に必要なのでメモ
入力に1GBのファイルを使用したところどのような構成にしても全て16個のMapTaskが生成された

MapTaskの数 = 入力ファイルのサイズ ÷ 64MB

圧縮した場合はBZip2とGZipで試した結果コーデックに関わらず1つの入力ファイルは
1つのMapTaskに割り当てられるみたい

ブロックサイズを変更すれば分割するサイズを変更することが可能
"fs.s3n.block.size"で指定する(デフォルトは67108864)

// ブロックサイズを32MBに設定
conf.setInt("fs.s3n.block.size", (67108864 / 2));

EC2インスタンス数の上限

EC2の同時起動台数上限を超えてインスタンスを立ち上げようとしたときキャンセルされる
(ステータスはFAILEDとなる)

Job flow failed with reason: The requested number of instances exceeds your EC2 quota

EC2のインスタンス同時起動台数はデフォルトで20台なのでEMRを使用する場合は
余裕をもって使用できるよう増やしておく

圧縮について

ローカルからS3への転送がすごく遅いので圧縮した方が嬉しいことが多い
圧縮ファイルを入力に渡すとHadoopが解凍してくれるので圧縮するだけで使用可能。

スプリット 速度 圧縮率
BZip2 遅い 高い
GZip × 速い
Snappy × かなり速い 低い

性能について

MapTaskは分割した分だけ台数を用意すると性能おおよそ理論値通りに動作した

Map処理完了までの時間 = (MapTask数 ÷ インスタンス数) × MapTaskの実行時間

ReduceTaskはS3への出力に時間がかかるようで起動設定に100台とか設定しても
S3への出力はReduceTask(出力先毎)に1台のインスタンスが担当するため出力先が
2つであれば2台のインスタンスで最後は頑張ることになる
ReduceTaskを分けて出力を複数のファイルに分割するか出力を圧縮すると性能向上が
見込めるのではないかと思う

EntityTooLarge Your proposed upload exceeds the maximum allowed size

以前作成したブラウザからS3へ直接ファイルをアップロードする画面を使って
300MBほどのデータをアップロードしたところ以下のようなエラーが発生した

HTTP/1.1 400 Bad Request
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2//EN">

<html>
<head>
    <title></title>
</head>

<body>
    <code>EntityTooLarge</code>Your proposed upload exceeds the maximum allowed
    size105258698B06FF034709E32IEq3fcwyZVtnEvU0mHBUVsz+Rvl7jZAyCPGYqsKuZU5fr5f/6WrbJ8hZsIzGFbI61048576
</body>
</html>

Amazon S3: Browser-Based Uploads using POST
シグネチャ生成時のポリシー「content-length-range」にアップロードを許可するファイルの
サイズ(最小値と最大値)を設定することができるらしい

String policy_document = "{\"expiration\": \"" + limit + "\","
	+ "\"conditions\": ["
	+ "{\"bucket\": \"" + bucket + "\"},"
	+ "[\"starts-with\", \"$key\", \"" + path + "\"],"
	+ "{\"acl\": \"private\"},"
	+ "{\"success_action_redirect\": \"" + redirectURL + "\"},"
	+ "[\"starts-with\", \"$Content-Type\", \"\"],"
	+ "[\"content-length-range\", 0, 1048576]" + "]" + "}";

ソースを確認したら「1048576(100MB)」となっていたので修正したら大丈夫でした

java.lang.NoClassDefFoundError: org/codehaus/jackson/map/deser/std/StdDeserializer

Jersey-JSONとAWS-Java-SDKの併用時はjacksonのバージョンに注意

java.lang.NoClassDefFoundError: org/codehaus/jackson/map/deser/std/StdDeserializer


StdDeserializer.StringDeserializer (Jackson JSON Processor)

1.9系からはStdDeserializerを使用しないようになっています。

エラー発生時のpom.xmlを確認

・・・
<dependency>
	<groupId>com.amazonaws</groupId>
	<artifactId>aws-java-sdk</artifactId>
	<version>1.3.20</version>
</dependency>
・・・
<dependency>
	<groupId>com.sun.jersey</groupId>
	<artifactId>jersey-json</artifactId>
	<version>1.14</version>
</dependency>
・・・

依存関係を見るとaws-java-sdk(v1.3.20)ではjackson(v1.8.9)を使用しているので
先に書くとv1.8.9が使用される
f:id:yustam:20121003122800j:plain

pom.xmlを修正し依存関係の順番を変更

・・・
<dependency>
	<groupId>com.sun.jersey</groupId>
	<artifactId>jersey-json</artifactId>
	<version>1.14</version>
</dependency>
・・・
<dependency>
	<groupId>com.amazonaws</groupId>
	<artifactId>aws-java-sdk</artifactId>
	<version>1.3.20</version>
</dependency>
・・・

f:id:yustam:20121003122138j:plain
これでjackson1.9系を使用するようになる

APIからS3のディレクトリを削除する

S3のディレクトリを配下のオブジェクトごと一括で削除したいと思ったのですが
ディレクトリを削除するというAPIは用意されていないようでオブジェクトを
ひとつずつ削除する必要があるそうです
AWS Developer Forums: Delete folder using Java API ...

if (s3Client.doesBucketExist(BUCKET_NAME)) {
    ObjectListing objects = s3Client.listObjects(BUCKET_NAME, PREFIX);
    for (S3ObjectSummary objectSummary : objects.getObjectSummaries()) {
        s3Client.deleteObject(BUCKET_NAME, objectSummary.getKey());
    }			
}

AmazonLinuxにGitLabを導入

GitLabをAmazon EC2の所謂Amazon Linuxにセットアップしてみました。
今回使用したAMIのIDは『ami-2819aa29』です。

gitlabhq/doc/installation.md at stable · gitlabhq/gitlabhq · GitHub
CentOS6にGitLabをインストールする方法 | Ryuzee.com
こちらの記事に丁寧に書かれていたのでほとんどその通りなのですが
Amazon Linuxでは少し足りない部分があったのでメモ程度に

Amazon Linuxではrootでsshできないのでec2-userでログインしsudoで実行
最初に以下のコンポーネントが足りないので追加インストール

yum install make
yum install gcc
yum install gcc-c++
yum install sqlite-devel

checkinstallがうまくインストールできなかったのでruby1.9.3はそのままインストール

cd /tmp/ruby-1.9.3〜
./configure
make
make install

bundle install実行時にsqlite3-1.3.6が無いと言われるのでインストール

Could not find sqlite3-1.3.6 in any of the sources

gem install sqlite3-ruby

最後にgitlab.confにてポートを80番以外にする場合はListenを追加

Listen 8080