GitBucket 4.1をリリースしました

Scalaで実装されたオープンソースのGitサーバ、GitBucket 4.1をリリースしました。

https://github.com/takezoe/gitbucket/releases/tag/4.1

今回はUIの崩れなど細かい部分の修正がメインですが、大きめの変更として以下の3点があります。

SSHの接続をgitユーザに統一

SSH経由でのGitリポジトリへの接続をgitユーザで行うようになりました。これによって従来の

ssh://root@localhost:29418/root/test.git

のようなURLではなく

ssh://git@localhost:29418/root/test.git

ブランチプロテクションのUI改善

ブランチプロテクションのUIが若干紛らわしい部分があったため、以下のように改善しました。

  1. チェックするステータスが存在しない場合は"Require status checks .."チェックボックスは無効になります。 f:id:takezoe:20160530113844p:plain
  2. "Require status checks .."がチェックされてもチェックするステータスが選択されていない場合は送信ボタンは無効になります。 f:id:takezoe:20160530113849p:plain
  3. "Require status checks .."がチェックされチェックするステータスが選択された場合のみ送信ボタンが有効になります。 f:id:takezoe:20160530113853p:plain

プルリクエストのタイトルの初期値

プルリクエスト作成時にコミットメッセージもしくはブランチ名からタイトルの初期値を生成するようにしました。

f:id:takezoe:20160601055227p:plain

この他にも様々な改善やバグフィックスを行っています。詳細についてはIssueの一覧をご覧ください。

Akka HTTPでJSONを使ってみる

Akka HTTPの入門についてはセプテーニさんのこちらの記事がわかりやすいです。

labs.septeni.co.jp

APIサーバにAkka HTTPを使ってみようかなぁと思っているので試しにリクエスト、レスポンスにJSONを使うのをやってみました。spray-jsonを使うモジュールが用意されているみたいなので依存関係に追加します。

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-http-experimental" % "2.4.6",
  "com.typesafe.akka" %% "akka-http-spray-json-experimental" % "2.4.6"
)

コードは以下のような感じ。ちょっと関係ない部分も入っちゃってますが、ケースクラスのプロパティ数に応じてjsonFormat1jsonFormat22を使ってimplicit valを定義しておけばいいみたい。

import akka.actor._
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.HttpRequest
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import spray.json.DefaultJsonProtocol._

object HelloApiServer extends App {

  // JSONをマッピングするケースクラス
  case class HelloRequest(name: String)
  case class HelloResponse(message: String)

  // JSON変換に必要なJsonFormatを定義
  implicit val helloRequestProtocol = jsonFormat1(HelloRequest)
  implicit val helloResponseProtocol = jsonFormat1(HelloResponse)

  implicit val actorSystem = ActorSystem("my-system")
  implicit val flowMaterializer = ActorMaterializer()

  val route = get {
    pathEndOrSingleSlash {
      handleWith((request: HttpRequest) => "API is ready.")
    }
  } ~ path("hello") {
    post {
      entity(as[HelloRequest]){ request =>
        complete {
          HelloResponse(message = s"Hello, ${request.name}!")
        }
      }
    }
  }

  val serverBinding = Http(actorSystem)
    .bindAndHandle(route, interface = "localhost", port = 8080)

}

バックエンドの処理ではJSON変換に主にJacksonを使っているのですが、Akka HTTPを使うならspray-jsonに統一したほうがいいかなぁ。

組み込みMySQLを使ったユニットテスト

GitBucketがMySQLPostgreSQL対応したのでマイグレーションのテストをMySQLPostgreSQLで実行できるようにしたいなぁと思って方法を考えています。

テスト用のDBを立てたりDockerを使ったりするのが一般的な方法なのではないかと思いますが、Javaで利用可能な組み込みMySQLなんていうものも存在するようなので試してみました(以前@makingに教えてもらいました)。

github.com

使い方はとても簡単で、Mavenの依存関係を追加して

<dependency>
  <groupId>com.wix</groupId>
  <artifactId>wix-embedded-mysql</artifactId>
  <version>1.0.3</version>
  <scope>test</scope>
</dependency>

こんな感じで使えます。

MysqldConfig config = aMysqldConfig(v5_7_10)
  .withPort(3306)
  .withUser("root", "")
  .build();

EmbeddedMysql mysqld = anEmbeddedMysql(config)
  .addSchema("gitbucket")
  .start();

Connection conn = DriverManager.getConnection(
  "jdbc:mysql://localhost:3306/gitbucket", "root", "");
...

mysqld.stop();

初回実行時はプラットフォームに応じたMySQLバイナリをダウンロードしてくるのでめっちゃ時間がかかりますw ただ、設定もコードで制御できますし、Dockerとか別途整備しなくても使えるのはいいですね。

ちなみにMySQLの組み込み化には以下のツールを使っているようです。

github.com

MySQLの他にもこのツールを使ってMongoDB、Redis、Memcached、node.js、PostgreSQLを組み込み化するライブラリが存在するようです。また、これとは関係ないようですがMariaDBを組み込み化しているJavaライブラリもあったりします。

github.com

現状では自動テストでこういった外部のミドルウェアが必要な場合はやはりDockerを使うのが筋がいいとは思うのですが、状況によってはこれらのライブラリが有効な場合もあるのではないでしょうか。

IntelliJのScaladocのインデントの設定

以前書いたIntelliJのScaladocのインデント設定ですが…

takezoe.hatenablog.com

IntelliJScalaプラグインをバージョンアップしたら場所が変わっていました。以前の設定も引き継いでくれないみたいです。

f:id:takezoe:20160523003800p:plain

「ScalaDoc」タブの「Use scaladoc indent for leading asterisk」というチェックボックスをOFFにすると以前と同じ挙動になります。

Scala用のシンプルなJDBCラッパーを作ってみた

以前からちょっとしたものを作るときに便利なIO関連のユーティリティをまとめたScalaライブラリを作っていたのですが、最近ScalaのIOライブラリはbetter-filesがよさげなのでこちらに乗り換えるのがいいかなと思ったところ、自作ライブラリにあるJDBC周りの機能がbetter-filesにはなかったので自作ライブラリから単独で使えるライブラリとして切り出してみました。

github.com

こんな感じの依存関係をbuild.sbtに追加して、

resolvers += "amateras-repo" at "http://amateras.sourceforge.jp/mvn/"

libraryDependencies += "com.github.takezoe" %% "better-jdbc" % "1.0.0"

以下のインポート文で使えるようになります。

import better.jdbc._

SELECTはこんな感じ。SQLは文字列またはsqlというString Interpolationでパラメータを埋め込んだものを渡すことができます。コールバックにResultSetが渡されるのでそこで必要な値を抽出します。DBUtilsみたいな感じですね。

val users: Seq[(Int, String)] = DB.autoClose(conn) { db =>
  db.select("SELECT * FROM USERS"){ rs =>
    (rs.getInt("USER_ID"), rs.getString("USER_NAME"))
  }
}

selectFirstで先頭の1件だけをOptionで取得できます。カウントとかを取得する場合用にselectIntとかselectStringという便利メソッドもあります。

val user: Option[(Int, String)] = DB.autoClose(conn) { db =>
  db.selectFirst(sql"SELECT * FROM USERS WHERE USER_ID = $userId"){ rs =>
    (rs.getInt("USER_ID"), rs.getString("USER_NAME"))
  }
}

ファクトリメソッドを渡してケースクラスにマッピングすることもできます。

case class User(userId: Int, userName: String)

val users: Seq[User] = DB.autoClose(conn) { db =>
  db.select("SELECT USER_ID, USER_NAME FROM USERS", User.apply)
}

更新系はupdateメソッドを使います。

DB.autoClose(conn) { db =>
  db.update(sql"INSERT INTO USERS (USER_ID, USER_NAME) VALUES ($userId, $userName)")
}

トランザクションはこんな感じ。

DB.autoClose(conn) { db =>
  db.transaction {
    db.update(sql"DELETE FROM GROUP WHERE USER_ID = $userId")
    db.update(sql"DELETE FROM USERS WHERE USER_ID = $userId")
  }
}

簡単なものですが、こういうの何度も自作している気がするのですぐに使える状態のライブラリがあると便利かなと思います。

Lagomのクラスタリングとデプロイ

Lagomを試してみるシリーズ第13回です。今回はクラスタリングとデプロイについてです。実際に動かしてみるのは面倒なのでここはドキュメントを読むだけに留めておきたいと思います。

クラスタリング

LagomはAkka Clusterでクラスタリングが可能なようです。

クラスタの管理はConductRで行う方法と明示的に設定する方法があり、明示的に設定する場合は各ノードのapplication.confにシードノードの設定を記述します。

akka.cluster.seed-nodes = [
  "akka.tcp://MyService@host1:2552",
  "akka.tcp://MyService@host2:2552"
]

このあたりも内部的にAkkaを使っているメリットを感じます。

デプロイ

また、デプロイについてはsbt-native-packagerを使用してパッケージングを行えるようになっており、

$ ./activator universal:packageBin

などのようにすると各プロジェクトのtarget/universalディレクトリ配下にzipファイルを生成されるのでこれを適当なサーバにコピーして展開し、binディレクトリにあるスクリプトを実行することで起動できます。開発時と異なりCassandraは別途起動しておく必要があります。

sbt-native-packagerを使用しているのでzipファイル以外にもMSIDMG、Dockerコンテナなどにもパッケージングできるようです。あとはドキュメントにはConductR使うといいよみたいなことが書いてありますが華麗にスルーすることにしますw

さて、これで残るはログの設定やAkkaとのインテグレーションなのですが、今回はLagomを使うことが目的ではなく、Lagomからフレームワークとして学ぶことがあるのではという趣旨なのでLagomを試してみるシリーズはいったんこれで終了にしたいと思います。

まとめ

Lagomを一通り見てみて、基盤としてAkkaやAkka Streamsをうまく活用しているなと感じました。また、サービスの定義と実装でプロジェクトを分ける、サーキットブレーカーやイベントソーシング + CQRS機構を標準装備しているなど、スケーラブルなマイクロサービスにおけるプラクティスをフレームワークレベルで提供しているところも興味深いところです。

ただ、実用面で考えるとサーキットブレーカーはFinagleも標準装備しているようですし、イベントソーシング + CQRS機構も実際問題嬉しいかというと微妙な気がします。Javaで記述できるといっても実際はかなり辛い感じなので今のところ積極的にLagomを選択する動機はあまりないような気がしています。

まあ、まだ出たばかりですし、しばらくは様子見という感じでしょうか。

LagomにおけるJSONのシリアライズとマイグレーション

もはや若干やけくそ感もあるLagomを試してみる第12回です。今回はLagomにおけるJSONシリアライズ/デシリアライスを見ていきたいと思います。

ここまで見てきたようにLagomはHTTPやWebScoketなど外部とのやりとりにはJSONを使用しますが、コマンドやイベントといった内部のアクター間のメッセージのやり取りにもJSONを使うことが推奨されており、Jsonableというマーカインターフェースを実装していればJSONシリアライズ/デシリアライズしてくれるようです。また、大きなメッセージの場合は代わりにCompressedJsonableインターフェースを実装することで圧縮を行うこともできるようです。

LagomのJSON変換はJacksonが用いられており、デフォルトでは以下のモジュールが有効になっているようです。

# The Jackson JSON serializer will register these modules.
# It is also possible to use jackson-modules = ["*"] to dynamically
# find and register all modules in the classpath.  
jackson-modules = [
  "com.fasterxml.jackson.module.paramnames.ParameterNamesModule",
  "com.fasterxml.jackson.datatype.jdk8.Jdk8Module",
  "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule",
  "com.fasterxml.jackson.datatype.pcollections.PCollectionsModule",
  "com.fasterxml.jackson.datatype.guava.GuavaModule"]

加えてJavaオブジェクトとJSONの変換を行うだけでなく、JSONスキーマ変更時の差分を吸収するための機能も提供しています。

プロパティの削除は特に気にせずフィールドを削除すればいいのですが、追加する場合でなおかつ初期値が必要な場合は@Value.Defaultアノテーション付きのメソッドを定義しておけばいいようです。

@Value.Immutable
@ImmutableStyle
@JsonDeserialize(as = ItemAdded.class)
public interface AbstractItemAdded extends Jsonable {
  ...
  @Value.Default
  default String getNote() {
    return "";
  }
}

フィールドのリネームや構造の変更など、複雑なマイグレーションの場合はJSONをバージョニングしておき、バージョン番号を見て任意の変換処理を走らせることができます。

この場合、以下のようにJacksonJsonMigrationを継承したマイグレーションクラスを作成する必要があります。

public class ItemAddedMigration extends JacksonJsonMigration {

  @Override
  public int currentVersion() {
    return 2;
  }

  @Override
  public JsonNode transform(int fromVersion, JsonNode json) {
    ObjectNode root = (ObjectNode) json;
    if (fromVersion <= 1) {
      root.set("itemId", root.get("productId"));
      root.remove("productId");
    }
    return root;
  }

}

そしてapplication.confJSONに変換するクラスとマイグレーションクラスを紐付けます。

lagom.serialization.json.migrations {
  "com.myservice.event.ItemAdded" = "com.myservice.event.ItemAddedMigration"
}

バージョンはアクターのメッセージのマニフェストに格納されているようです。Lagomのシリアライズ/デシリアライズの仕組みはAkkaのシリアライズ機構をうまく活用しているように見えますね。