GitBucket 4.27.0をリリースしました

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

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

ブラウザ上でタグを作成可能に

リポジトリビューア上でタグを作成できるようになりました。

f:id:takezoe:20180728173917p:plain

EditorConfigのサポート

様々なテキストエディタIDEでコーディングスタイルを統一するためのEditorConfigの設定の一部をGitBucketのコードビューアとエディタでサポートしました。リポジトリのルートディレクトリに.editorconfigがあると、以下の設定が反映されます。

  • indent_style
  • indent_size (tab_width)
  • end_of_line

イシュー/プルリクエスト検索の改善

これまではイシュー/プルリクエストの検索結果は同一ページに表示されており、イシューとプルリクエストの区別がつかなかったのですが、今回のバージョンからは別々のページに表示されるようになりました。また、検索結果にクローズされているかどうかも表示されるようになりました。

f:id:takezoe:20180728173931p:plain

今回のバージョンではこの他にも様々な改善やバグフィックスを行っています。特にプラグインのインストールやプルリクエストのコメント周りについては多くの改善を含んでいます。詳細についてはIssueの一覧をご覧ください。

Scalaをはじめよう! ─マルチパラダイム言語への招待─

技術書典で販売されていた同人誌がインプレスR&Dさんから電子書籍&オンデマンドで発売されたものとのことで、Scala初心者向けの入門用書籍にどうだろうと思い、Kindle版を購入して読んでみました。

内容的にはScalaの基本的な部分がわかりやすく解説されています。IDEではなくsbtだけで進めていくのも環境構築でつまづく可能性が減るので入門書としてはよい判断なのではないかと思います。とはいえ、本当のプログラミング初心者ではなく、他の言語で一定のプログラミング経験のある方を対象読者として書かれているようです。説明がやや簡素すぎるのではと感じたり、理系の論文ぽい言い回しが気になる部分もありますが、想定されている対象読者層であれば問題ないのかもしれません。

内容については前から読み進めていくことでクラス、トレイト、オブジェクト、ケースクラス、for式、パターンマッチ、コレクション、implicitなどScalaでコードを書く際に押さえておくべきトピックが順次登場してきますし(個人的にはこの流れならFutureはなくても良かったかもという気もしますが)、モナドのような最初から理解していなくても問題ないトピックについてはコラムの形で解説されているなど、Scalaのエッセンスを切り捨てることなく、かつできるだけわかりやすく説明しようという工夫がなされています。

もちろん実際に業務でScalaを使うにはこの他にも様々な知識が必要になりますが、他の言語でプログラミング経験のある方が初めてScalaに触る際の手引きとしてはなかなか良い書籍なのではないかと思いました。

Akka StreamsのElasticsearchコネクタでupdateやdeleteもできるようにした

AlpakkaにはElasticsearch用のコネクタも含まれていて、これは元々自分がコントリビュートしたものなのですが、これまではFlow/Sinkによるバルク書き込みはindexupsertにしか対応していませんでした。また、1つのストリームでindexupsertを混ぜることもできなかったのですが、最近わけあってupdatedeleteも含め1つのストリームで処理したい事情があったのでできるようにしてみました。

github.com

このプルリクはすでにマージされているのでAlpakkaの次のリリース(Alpakka 0.21)から利用可能になると思います。

使い方は以下のような感じで、indexupdateupsertdeleteそれぞれの操作に応じたメッセージを生成するだけです。すべてのメッセージをIncomingIndexMessageもしくはIncomingUpsertMessageにすればこれまでと同じ動作になります。

val requests = List[IncomingMessage[Book, NotUsed]](
  IncomingIndexMessage(id = "00001", source = Book("Book 1")),
  IncomingUpsertMessage(id = "00002", source = Book("Book 2")),
  IncomingUpsertMessage(id = "00003", source = Book("Book 3")),
  IncomingUpdateMessage(id = "00004", source = Book("Book 4")),
  IncomingDeleteMessage(id = "00002")
)

val f = Source(requests)
  .via(ElasticsearchFlow.create[Book]("books", "_doc"))
  .runWith(Sink.seq)

これでただ単に流れてきたデータを突っ込むだけでなく、更新や削除もできるようになるので、Akka Streams + Elasticsearchの用途がもう少し広がるのではないかと思います。

Akka Streamsで巨大なXMLをストリーム処理する

Akka Streams用のコネクタを提供するAlpakkaにはXMLサポートも含まれており、XMLの読み込み・書き込みを行うためのFlowやSinkを利用することができます。

しかし読み込みはXMLのパースイベントをストリームするだけなので、実際には下流の処理で状態を管理したり、イベントをスタックするなどしてXMLの構造を判別する必要があります。複雑な構造のXMLの場合、これはかなりしんどいことになります。というわけでパスを指定するとその要素をorg.w3c.dom.Elementとして扱うことができるFlowを作ってみました。

github.com

このプルリクエストはすでにマージされていますので、Alpakkaの次のリリースから利用できるようになるかと思います。

使い方は以下のような感じでXmlParsing.parserと組み合わせて使います。ここではAkka Streams標準のFileIOを使ってファイルから読み込んでいますが、ByteStringを出力するコネクタであればなんでも使うことができます。

val graph = FileIO.fromPath(Paths.get("dataset.xml"))
  .via(XmlParsing.parser)
  .via(XmlParsing.subtree("doc" :: "elem" :: "item" :: Nil))
  .map { element =>
    ...
  }
  .to(Sink.ignore)

パースイベントのストリームと比べるとメモリ消費量は大きくなると思いますが、まあどっちみちXMLから情報を抽出するのであれば同じような処理を自分でやらないといけないと思いますし、DOM Elementなら他のXMLライブラリとも組み合わせて使いやすいかなと。

最近は巨大なデータを扱う場合はXMLではなく、より効率的なフォーマットを使うことが多いと思いますが、諸事情によりデータソースがXMLというケースもままあるのではないでしょうか。Akka StreamsとAlpakkaを使えば繰り返し構造を持つXMLを別のデータ形式に変換したり、データベースに格納するみたいな処理をシュッと書くことができるんじゃないかなと思います。

AWS Kinesisからの読み込みをAkka Streamsで行う

Akka Streams用の様々なコネクタを提供するAlpakkaですが、もちろんAWS Kinesis用のコネクタも含まれています。

しかし、書き込むためのFlow/Sinkは普通に使える感じのものなのですが、読み込むためのSourceはライセンスの問題からKCLを使ったコネクタを取り込むことができず、AWS SDKを使ったものしか含まれていません。このSourceは以下のような感じでシャード毎の設定を明示的に指定する必要があったり、チェックポイントの記録なども自前で行う必要があるなどそのまま使うにはちょっと厳しさがあります。

val mergeSettings = List(
  ShardSettings("myStreamName",
                "shard-id-1",
                ShardIteratorType.AT_SEQUENCE_NUMBER,
                startingSequenceNumber = Some("sequence"),
                refreshInterval = 1.second,
                limit = 500),
  ShardSettings("myStreamName",
                "shard-id-2",
                ShardIteratorType.AT_TIMESTAMP,
                atTimestamp = Some(new Date()),
                refreshInterval = 1.second,
                limit = 500)
)

val mergedSource: Source[Record, NotUsed] = 
  KinesisSource.basicMerge(mergeSettings, amazonKinesisAsync)

AlpakkaのドキュメントではKCLを使ったサードパーティのSourceの実装が2つ紹介されているのですが、以下のものを試してみました。

github.com

使い方はこんな感じです。InitialPositionInStream.LATESTを指定しているので未処理のレコードのみ取得されます。読み込んだレコードはmarkProcessed()メソッドで処理済みとしてマークしておくと、処理したところまでの分がチェックポイントとしてDynamoDBに書き込まれます。

val consumerConfig = new KinesisClientLibConfiguration(
    "test-app",
    "test-stream",
    new DefaultAWSCredentialsProviderChain(),
    "kinesis-worker"
  )
  .withRegionName("us-east-1")
  .withCallProcessRecordsEvenForEmptyRecordList(true)
  .withInitialPositionInStream(InitialPositionInStream.LATEST)

KinesisSource(consumerConfig).map { record =>
  try {
    ...
  } finally {
    record. markProcessed() // 処理済みとしてマーク
  }
}.runWith(Sink.ignore)

このコネクタは内部でメモリ上で未処理のレコードを管理しており、フラグを立て忘れたレコードがあると以降チェックポイントが書き込まれず、最終的にはOutOfMemoryになってしまうのでmarkProcessed()メソッドは必ず呼び出されるように実装しておく必要があります。

チェックポイントはデフォルトでは60秒間隔で書き込まれているようですが、このあたりの挙動はapplication.confで設定を行うことで変更できます。

com.contxt.kinesis.consumer.shard-checkpoint-config {
  checkpoint-period = 60 seconds
  checkpoint-after-processing-nr-of-records = 10000
  max-wait-for-completion-on-stream-shutdown = 10 seconds
}

ストリームを終了する際の挙動は少し悩ましいところで、ActorSystemのterminationに反応して未書き出しのチェックポイントを書き出すという終了処理を行なっているようです。そのためKillSwitchでストリームを終了させただけだと終了時のチェックポイントの書き出しが行われませんでした。

チェックポイントが最後まで書き込まれずに終了してしまうと次回起動時に前回処理済みのレコードが再度取得されてしまいます。ただ、チェックポイントは非同期で書き込んでいる以上、異常終了などでそのような状態になってしまうことは考えられます。チェックポイントの書き出し間隔をある程度短くしておき、受信側は同じメッセージを二重に受信しても冪等になるよう作っておくのがよいかもしれません。

動作を確認しつつソースコードにも目を通してみたのですが、GitHubのスターこそ少ないもののそれなりにしっかり作られたコネクタだと思います。Alpakkaのドキュメントでは以下のコネクタも紹介されています。こちらはチェックポイントの書き込みを自動で行うためのFlowが付属しているようです。機会があればこちらも試してみたいと思います。

github.com

GitBucket 4.26.0をリリースしました

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

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

セントラルレジストリからのプラグインインストール

先日運用を開始したプラグインレジストリからインターネット経由でプラグインをインストールできるようになりました。

f:id:takezoe:20180627120403p:plain

プラグインレジストリについては以下のエントリを参照していただければと思います。

takezoe.hatenablog.com

ダッシュボードにRepositoriesタブを追加

ダッシュボードにログイン中のユーザが参照可能なリポジトリの一覧を表示するRepositoriesを追加しました。

f:id:takezoe:20180627120534p:plain

これに伴い、ダッシュボードのサイドバーは非ログイン時もログイン時も最近更新されたリポジトリの一覧に固定されました(これまではログイン時は参照可能なリポジトリの一覧が表示されていました)。

Forkダイアログの改善

"Fork"ボタンがサイドバーから画面の右上のボタンに移動しました。このボタンをクリックすることでフォークするアカウントを選択するダイアログが表示されます。

f:id:takezoe:20180627120801p:plain

ダイアログはカードスタイルからリストスタイルに変更されています。長いアカウント名でも省略されなくなったため、目的のアカウントを見つけるのが容易になっています。また、"Show forks"ボタンをクリックするとフォークの一覧を表示することができます。

f:id:takezoe:20180627120933p:plain

クイックプルリクエストのサジェストを抑制

リポジトリビューアにはブランチからワンクリックでプルリクエスト作成画面に遷移できるサジェストが表示されますが、この表示条件を以下のように修正することにより、不要なサジェストの表示を抑制しました。

  • A last committer of the branch is the logged-in user
  • A last commit of the branch is within one hour
  • The branch isn't behind of the default branch

f:id:takezoe:20180627121139p:plain

未完了のタスクリストの表示

プルリクエストでは古いコミットに対するコメントはデフォルトでは折りたたまれて表示されますが、未完了のタスクリストを含むコメントについては折りたたまずに表示するようになりました。

f:id:takezoe:20180627121326p:plain

プラグイン向けに新しい通知フックを追加

通知に関して、以下のフックを追加しました。

  • assigned
  • closedByCommitComment

実際に通知を有効にするにはGitBucketのバージョンアップ後にgitbucket-notification-pluginを最新版に更新する必要があります。

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

GitBucketのプラグインレジストリの運用を始めました

f:id:takezoe:20180618024108p:plain

GitBucketはプラグイン機構を持っているのですが、Jenkinsのようにインターネット経由でインストールできるようにしたいという考えはGitBucketを作り始めた頃からありました。また、実際にプラグイン機構を導入してみると、本体の修正にあわせてプラグインの修正が必要な場合の検出が難しかったり、ソースコードの修正は不要でもバイナリ互換性の問題で再コンパイルが必要な場合があり、プラグインのメンテナンスコストの点で問題を感じていました。

そこで、インターネット経由でプラグインをインストールするためのセントラルレジストリ兼ビルドファームをここ1ヶ月ほどコツコツ作っていたのですが、ようやく形になってきたので以下で運用を初めてみました。

https://plugins.gitbucket-community.org/

このサイトは以下の機能を持っています。

  • プラグインの開発中バージョン(master)がGitBucket本体のmasterでビルドが通るかチェックする
  • プラグインの最新のリリース版(最新のタグ)がGitBucket本体のmasterでビルドが通るかチェックする
  • プラグインまたはGitBucketに新しいタグが打たれるとビルドしてプラグインのjarファイルをダウンロード可能にする

プラグインは最新のタグのバージョンがGitBucketのリリース毎に自動的にビルドされるという形になります(jarファイル名にGitBucketのバージョンが含まれるようになります)。インストール時はお使いのGitBucketのバージョン用にビルドされたjarファイルを使用することでこれまでのようにプラグインが動作するGitBucketのバージョンを気にする必要はなくなります。

現在はGitBucketのorganizationで開発されているプラグインしか登録されていませんが、将来的にはサードパーティ製のプラグインも登録できるようにする予定です。プラグイン作者の方のメンテナンスコストの低減、ユーザの利便性の向上の両方に繋がるのではないかと思っています。

GitBucket側でこのレジストリからプラグインをインストールできるようにする機能もすでに実装してあるのですが、次のリリースに含めるかどうかはまだ検討しているところです。

github.com