GitBucketに見る長期的なメンテナンスの必要なScalaアプリケーションにおいてScalaのバージョンアップのために留意しておくべきこと

Scalaのバージョンアップはなぜ大変なのか?

GitBucketはもうかれこれ5年近く開発を継続しており、Scalaのメジャーバージョンアップも何度か経験してきました。ScalaのバージョンアップはJavaと比べるとかなり苦労することが多いのですが、それはScalaのバージョンアップに伴うアプリケーションコードの修正よりも、むしろ以下の2点による部分が大きいのではないかと思います。

  1. フレームワークやライブラリの仕様がアグレッシブに変わる
  2. Scalaのメジャーバージョン間でバイナリ互換性が保証されていない

まず前者についてですが、これまでPlay FrameworkやSlickなど標準的なフレームワークやライブラリもかなり活発に仕様変更が行われてきました。フレームワークの開発が活発に行われているという状況はメリットも多い反面、後方互換性のない変更が多いとバージョンアップがつらいというデメリットもあります(とはいえ個人的にはフレームワークの開発が停滞しているよりは活発な方が好ましいと思いますし、最近は主要なフレームワークやライブラリについてはある程度落ち着いてきた感があります)。

後者については、Scalaはメジャーバージョンアップでのバイナリ互換性が保証されていないため、フレームワークやライブラリはScalaのメジャーバージョン毎にビルドする必要があります。このため、まずはすべての依存ライブラリが新しいバージョンのScalaに対応してからでないとScalaのバージョンをあげることができないという問題が発生します。中にはメンテの止まってしまっているライブラリが存在する可能性もありますので、それらへの対処も必要になります。

GitBucketの場合

GitBucketではWebフレームワークとしてScalatra、DBアクセスライブラリにSlickを使っています。他にも様々なライブラリを使用していますが、Javaライブラリが多く、Scalaのバージョンアップには影響していません。つまり、ScalatraとSlickというフレームワークについて上記の2点が主な課題となってきます。

Scalatra

Scalatraについてはすでに枯れているフレームワークであり、GitBucketで使い始めてから大きな仕様変更はありません。開発はさほどアクティブとは言えませんが、自分がコミッタをやっているのでScalaのバージョンアップ対応など必要な作業については自分で行なうことができるという安心感があります。

とはいえ、Scalatra自体様々なサブモジュールがあり、依存関係もそれなりに複雑です。Scalaのバージョンアップの際にはこれらの依存ライブラリにプルリクエストを出して回り、それらがリリースされてからScalatraをバージョンアップするということを行なっており、毎回数ヶ月がかりの作業になります。そのため、最近はメンテナンスコストを減らすため重要度の低いサブモジュールや依存ライブラリをなるべく減らす(小さなものでライセンス上問題なければScalatraのソースツリーに取り込んでしまうということも)という取り組みを行なっています。

Slick

Slickについては元々Typesafe社謹製のライブラリでScala業界ではデファクトスタンダードとして利用されていることもあり、メンテナンスを自分で行わないといけないというような状況には至っていません。しかし、Slick2から3へのバージョンアップでAPIがIOモナドを使った非同期方式に変更され、アプリケーションコードを大幅に書き換える必要がありました。また、当時Slick2はScala 2.12向けにはリリースされていなかったため、Scala 2.12にバージョンアップするにはSlick3に移行する必要がありました(その後有志の手によってSlick2もScala 2.12対応が行われ、Slick 2.1.0のScala 2.12版がリリースされています)。

GitBucketでは本体のコードの修正量もさることながら、IOモナドや非同期処理を導入することでプラグイン開発の難易度が上がってしまうのは避けたい事態でした。さらにGitBucketはWebフレームワークとしてScalatraを使っており、従来のサーブレットによる同期処理が基本となっているためDBアクセスを非同期に行うメリットが薄いという事情もありました。ScalikeJDBCやquillなど別のDBアクセスライブラリへの移行も検討したのですが、様々な事情から最終的にはblocking-slickという、Slick3上でSlick2互換の同期APIを提供するライブラリを自作し、これを用いてSlick3に移行するという選択をしました。

Scalaのバージョンアップのために留意しておくべきこと

冒頭でも書いたように、Scalaのバージョンアップの難しさは、アプリケーション本体のコードよりも、使用しているフレームワークや依存ライブラリによる影響の方が大きいというのがGitBucketを開発を通じての感想です。

Javaの場合、古いライブラリでも基本的に新しいバージョンのJavaでそのまま動作しますが、Scalaライブラリの場合、メンテナンスが止まってしまうとScalaのバージョンアップ時にblockerになってしまうため、長期的にメンテナンスする必要のあるアプリケーションでマイナーなフレームワークやライブラリを選択するのは大きなリスクになる可能性があります。マイナーなものを選択する場合は最悪自分でメンテナンスを行う覚悟をするか、別のものへの移行を常に視野に入れておく必要があります。

また、依存ライブラリについては必要最低限に留め、小さなものであれば敢えて自前で書いてしまうのも1つの手です。同じ機能を提供するJavaライブラリが存在するのであればそちらを利用したり、自前でライブラリを書く場合でもScala固有の機能が不要なものであればJavaで書いたものをScalaから使うのも有効な手段です。実際にGitBucketではMarkdownパーサやマイグレーションツールなどはJavaで書いた独自ライブラリをScalaから使用しています。

とはいえ、Scala界隈も黎明期の混沌とした状況からは脱して標準的なフレームワークやライブラリはだいぶ安定・収束しつつあり、メンテナンス性を考慮した選択も数年前と比べるとかなり容易になっています。Scalaのバージョンアップは新しい機能が利用できるようになったり、コンパイルが速くなったりなど様々なメリットがあるので、特に長期的にメンテナンスを行う必要のあるアプリケーションでは継続的に追従できる体制を整えておきたいところです。

akka-http-sessionによるセッション管理

Akka HTTPはプリミティブなHTTPツールキットであり、標準ではセッション管理の機能を持っていません。Akka HTTPにセッション管理機能を追加するためのライブラリとしてSoftwareMill社が開発しているakka-http-sessionというものがあります。

github.com

このライブラリを使うとAkka HTTPでセッション管理のためのDSLを利用できるようになります。セッション情報はカスタムHTTPヘッダまたはクッキーでクライアントに送信されます。設定で暗号化することができる他、オプションのモジュールを使用することでJWTを使用することもできます。また、CSRFプロテクション(クッキーの場合のみ)や長期間セッションを維持するためのリフレッシュトークンもサポートされています。

ここでは簡単な例を紹介します。まずはbuild.sbtに以下の依存関係を追加します。

"com.softwaremill.akka-http-session" %% "core" % "0.5.3"

セッションに格納するクラスを作っておきます。

case class Session(userId)

実際のコードは以下のような感じになります。Akka HTTP標準のDSLと同じ感じで記述することができます。特に難しいところはないと思います。SessionConfigの作成時にSessionUtil.randomServerSecret()でランダムなsecretを与えていますが、サーバを複数台立てる場合や再起動してもセッションが継続するようにしたい場合は設定ファイルに切り出して固定のsecretを与えるようにするとよいでしょう。

import com.softwaremill.session.SessionDirectives._
import com.softwaremill.session.SessionOptions._
import com.softwaremill.session.SessionResult._
import com.softwaremill.session._

...

val sessionConfig = SessionConfig.default(SessionUtil.randomServerSecret())
implicit val sessionManager = new SessionManager[Session](sessionConfig)

// ログイン
path ("login") {
  post {
    formFields("userId", "password"){ case (userId, password) =>
      if(userId == "admin" && password == "admin"){
        setSession(oneOff, usingCookies, Session(userId)) {
          complete(StatusCodes.OK)
        }
      } else {
        complete(StatusCodes.Unauthorized)
      }
    }
  }
} ~
// ログアウト
path ("logout") {
  post {
    invalidateSession(oneOff, usingCookies){
      complete(StatusCodes.OK)
    }
  }
} ~
// 要ログインなエンドポイント
path ("required_login"){ case (account) =>
  get {
    requiredSession(oneOff, usingCookies) { session =>
      complete(StatusCodes.OK)
    }
  }
}

設定はデフォルトではapplication.confで行います。デフォルトの設定はこんな感じになっているようです。

セッション情報をHTTPヘッダでやり取りするので巨大なデータの管理には向いていませんが、手軽に使えるのでちょっとした認証をかけたりするのに便利そうです。

GitBucket 4.21.0をリリースしました

4.21.0には本体およびバンドルされたプラグインにいくつかの問題があったため、修正した4.21.2をリリース済みです。こちらをご利用ください。 https://github.com/gitbucket/gitbucket/releases/tag/4.21.2

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

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

リリースページ

タグの一覧画面がリリース機能で置き換えられました。それぞれのタグにリリースノートを記述したり、リリースファイルを添付することができるようになりました。

f:id:takezoe:20180127021047p:plain

OpenID Connectをサポート

OpenID Connectを使ったログインが可能になりました。

f:id:takezoe:20180127021156p:plain

Google Identity ProviderおよびKeycloak用の設定方法についてはOpenID Connect Settingsを参照してください。

新しいDBビューア

これまでGitBucketは管理者向けにH2データベースに付属のWebコンソールを同梱していましたが、DBのURLやID/パスワードを入力しなくてはならなかったり、独自の機能を追加しようにも手を入れられなかったりといった問題がありました。

このバージョンでは従来のH2コンソールが新しい独自実装のDBビューアに置き換えられています。

f:id:takezoe:20180127021618p:plain

なお、以前のH2コンソールを引き続き使用したいという方向けにgitbucket-h2console-pluginというプラグインを提供しています。

サブモジュールのリンク先の変更

従来、サブモジュールのリンクはGitリポジトリのURLになっていたのですが、これはブラウザ上では直接参照できないケースが多いという問題がありました。今回のバージョンではサブモジュールのGitリポジトリが特定のパターンに一致する場合、GitリポジトリではなくWebインターフェースへのリンクを出力するようになりました。

今のところ、サポートしているのは以下のサービスです。

これら以外のリポジトリは今まで通りGitリポジトリへのリンクになります。

Close/Reopenボタンの改善

イシューやプルリクエストのClose/Reopenボタンはキャンセルボタンと間違って押してしまうというケースが報告されていたため、これらのボタンをドロップダウンから選択する方式に変更しました。

f:id:takezoe:20180127022210p:plain

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

GitBucketで使っているSlick用のちょっとした便利機能

GitBucketではずっとSlickを使っているのですが、クエリを簡単に記述するためにちょっとした便利機能を追加しています。

1つめはこんな感じのエクストラクタ。以前吉田さんにTwitterかなにかで教えてもらったもの。

object ~ {
  def unapply[A, B](t: (A, B)): Option[(A, B)] = Some(t)
}

Slickだとjoinとかmapするときに以下のようにネストしたタプルを多用するのですが、

PullRequests
  .join(Issues).on { case (t1, t2) => 
    t1.issueId === t2.issueId)
  }
  .join(Accounts).on { case ((t1, t2), t3) => 
    t1.userName === t3.userName
  }
  .join(Accounts).on { case (((t1, t2), t3), t4) => 
    t2.userName === t4.userName
  }
  .joinLeft(Milestones).on { case ((((t1, t2), t3), t4), t5) =>
    t2.milestoneId === t5.milestoneId
  }

これを以下のようにすっきり記述することができます。テーブルを追加するときもカッコの数をあわせたりする必要がないので楽です。

PullRequests
  .join(Issues).on { case t1 ~ t2 => 
    t1.issueId === t2.issueId)
  }
  .join(Accounts).on { case t1 ~ t2 ~ t3 => 
    t1.userName === t3.userName
  }
  .join(Accounts).on { case t1 ~ t2 ~ t3 ~ t4 => 
    t2.userName === t4.userName
  }
  .joinLeft(Milestones).on { case t1 ~ t2 ~ t3 ~ t4 ~ t5 =>
    t2.milestoneId === t5.milestoneId
  }

2つめはRep[Boolean]に対するimplicitクラス。これは確か同僚が発明したものだったような気がします。

implicit class RichColumn(c1: Rep[Boolean]){
  def &&(c2: => Rep[Boolean], guard: => Boolean): Rep[Boolean] =
    if(guard) c1 && c2 else c1
}

これは条件によってfilterの内容を変更したい場合に使います。こんな感じ。

Accounts filter { t =>
  (1.bind         === 1.bind) &&
  (t.groupAccount === false.bind, !includeGroups) &&
  (t.removed      === false.bind, !includeRemoved)
} sortBy(_.userName)

第二引数(!includeGroupsとか!includeRemovedのところ)がtrueになる場合のみ条件が有効になります。検索フォームとか作るときに便利です。

ちょっとしたものですが、Slickでクエリ書くときに面倒に感じる部分なのでこれで少しは記述が楽になっているかなという感じです。

ZooKeeperによる分散システム管理

最近作っているdistributed-git-serverではクラスタの状態管理にMySQLを使っているのですが、ロックをRDBで実装したり、ハートビートやノードの状態管理などを自前で実装する必要があり、だいぶ面倒だなぁと感じていました。ZooKeeperを使えばこのような課題をうまく処理することができるかもしれないと思い、この本を読んでみています。

個人的にはこれまであまり縁がなかったのですが、ZooKeeperは元々米Yahooで開発されたもので、Hadoop関連プロダクトやSolr、Kafkaなど様々な分散ミドルウェアクラスタの状態管理などの用途に利用されています。ざっくり言えば通知機能を持った高可用なディレクトリインデックスサービスのようなもので、これをうまく利用することで分散ミドルウェアにおける様々な課題を解決できる基盤となるソフトウェアです。

この書籍は基本的にはZooKeeperを使って分散ミドルウェアを開発する開発者向けに書かれており、ノードの管理、マスタの選出、タスクのキューイングなど分散ミドルウェアにおける一般的な課題について、ZooKeeperの機能をどのように利用すれば解決できるかが解説されています。また、ZooKeeperの内部構造についても説明されているのでZooKeeperを使ったミドルウェアを利用する場合にも役に立つと思いますし、分散ミドルウェアを開発している、もしくは興味のある人であればZooKeeperを使わないにしても同じような仕組みを実装することになるであろうと思われるので参考になる部分があるのではないでしょうか。

スケーラブルで冗長性のあるGitサーバをどう作るか?

GitBucketでの長年の課題の1つがGitリポジトリのスケールアウトと冗長化でした。Gitリポジトリを格納するストレージに分散ファイルシステムを使うというのは1つの解決策になりますが、分散ファイルシステムの運用はなかなか大変です。もっと手軽に使い始めて徐々にスケールできる方法はないものかと考えていたのですが、1つ思いついたアイデアを試すために実験的にコードを書き始めてみました。

github.com

基本的なアイデアは普通のGitサーバを複数台用意しておき、リポジトリを分散配置するというものです。例によってScalaとJGitを使って実装されています。

f:id:takezoe:20180114032907p:plain

Gitクライアントからのリクエスト(pushやfetch)はフロントコントローラーでルーティングされ、リポジトリの配置されたノードにプロキシされます。リポジトリは全く同じものが複数ノードに存在するので1ノードが落ちても動作を継続することができます。落ちたノードが復旧すると生存しているノードと同期して自ノードのリポジトリを最新化します。

コントローラーは各ノードのディスク使用率を把握しており、新しくリポジトリを作成したり、リポジトリの再配置を行う際はディスク使用率を参照してどのノードに作成するかを決定します。1リポジトリで1ノードのディスクを食い尽くしてしまうような巨大なリポジトリがあると厳しいですが、理論上はディスクが足りなくなってきたら新しいノードを足していくだけでOKということになります。

ちなみに後から知ったのですが、このアプローチはGitHub社のDGit(現在はSpokesに改名されているようです)と似ています。以下のブログに色々と情報が書かれているので参考になります。

githubengineering.com

githubengineering.com

上記のブログからではわからない点として、WebブラウザからのGit操作(ブラウザでファイルを編集したり、プルリクエストをマージしたり等)をどのように実現しているのか?という点があります。

GitBucketではファイルシステム上のbareリポジトリを直接操作しているのですが、この方法ではリモートリポジトリを冗長化するのでそういうわけにはいかなくなります。上記のブログからはSpokesでも同じようにgitクライアントからのリクエストのレベルでプロキシしているように見えるので、Webアプリケーションがローカルリポジトリを持っていてそこをいじってからSpokesにpushしているのかなぁと推測されますが、実際どうなっているのか気になるところです。

Scala向けの高速ビルドツールBloopについて

少し前にScala CenterからBloopというビルドツールがアナウンスされました。

github.com

実際に動かしてみたわけではないのですが、READMEやソースコードなどからどのようなものかは大体掴めるので簡単にまとめておきます。興味のある方は是非試してみて欲しいです。

Bloopはビルドツールと言ってもsbtを置き換えるものではありません(そもそもBloop自身はビルドツールを名乗っていません)。簡単に言えばnailgunでサーバを立ち上げてzincでインクリメンタルコンパイルするためのツールで、コーディング中のコンパイル確認用途に通常のビルドツールの代わりに使用することでコーディング→コンパイル確認のラウンドトリップを高速化することを狙っているようです。特にsbt以外のビルドツールを使っている場合にビルドツール側の対応を待たなくても最新のzincが使える点がメリットとして挙げられています。

なので、基本的には既存のビルドツールを使用しつつ、コーディング中はBloopを使うというスタイルになります。

なお、Bloop自体はソースディレクトリやコンパイルに必要なクラスパスなど必要最低限の設定で動作します。既存のビルドツール用の設定からこれらの情報を抽出して使用することが想定されているようですが、現状ではsbt用のプラグインしかないようです。

肝心のコンパイル速度ですが、以下のアナウンス時のブログ記事に、いくつかの中〜大規模Scalaプロジェクトをsbtでコンパイルした場合とBloopを使った場合のコンパイル時間の比較結果が掲載されています。

http://www.scala-lang.org/blog/2017/11/30/bloop-release.html

Sparkは1.4倍とBloopを使う恩恵が大きそうですが、sbtやScala本体だと1.1倍程度と微妙なところです。ただし、これはクリーンビルドの場合なので、本来Bloopのユースケースとして想定されているであろう部分ビルドの場合はまた変わってくるでしょうし、GradleやMavenでビルドしている場合はもっと大きな効果が見込めるのではないかと思います。

とはいえ、コンパイルがsbtプラグインに依存している場合は当然Bloopだけではビルドすることができないので結構使いどころが限られそうな感じがありますし、そもそもScalaをsbt以外のビルドツールでビルドしているのも割とレアケースなのではという気もします(何か理由があってsbtを使えないケースだとするとBloopも使えない場合が多そう)。また、Scala製大規模ソフトウェアのビルドの高速化に本気で取り組むのであればPantsなどの選択肢もあります。

ちょっと立ち位置が微妙な気がしなくもないのですが、手軽に導入できそうではあるのでとりあえず試してみるのはありかもしれません。