Jekyllで未来の日付のエントリも表示する

Jekyllは以下のようにエントリの先頭にYAMLメタデータを記述することができます。

---
layout: post
title: "GitBucket 4.3 released!"
date: 2016-07-30 00:00:00
image: /images/gitbucket-4.2/adminlte.png
categories: gitbucket
---

dateで公開日を指定しておくと、その日までそのエントリは公開されません。その日時よりも後にサイトをビルドしなおしたタイミングで初めて表示されるようになります。

しかし、ローカルで表示を確認する場合など未来の日付のエントリの内容も表示したいことがあります。このような場合はJekyllに--futureコマンドをつけて起動すればいいようです。

$ jekyll server --baseurl '' --watch --future

なお、エントリの表示を制御する方法はdateの他にもいくつか方法があります。

たとえばエントリのメタデータに以下のように記述しておくとそのエントリは表示されなくなります。--unpublishedオプションを付けてJekyllを起動するとこのエントリも表示されます。

published: false

また、エントリのファイルを_posts/ディレクトリではなく_drafts/ディレクトリに置いておくことでも表示されなくなります。この場合は--draftsオプションを付けてJekyllを起動すると表示できるようです。

GitBucketをPostgreSQLやMySQLで動かす

GitBucketは標準では内蔵のH2という組み込みデータベースで動作しますが、4.0からはH2の代わりに外部データベースとしてPostgreSQLMySQLを使うことができるようになっています。H2はデータの保全性に問題がありますので業務等の重要な用途にGitBucketを使われるのであればなるべくPostgreSQLもしくはMySQLを利用することが望ましいです(パフォーマンス面でもメリットがあります)。

外部データベースの設定方法は以下のドキュメントに記載されていますが、このエントリでは日本語でもう少し丁寧に解説しようと思います。

github.com

クリーンインストールの場合

まず、一度GitBucketを起動すると~/.gitbucketディレクトリが作成されるので、いったんGitBucketを停止します。

~/.gitbucketディレクトリの中にdatabase.confというファイルがあるはずです。このファイルは以下のような内容になっています。

db {
  url = "jdbc:h2:${DatabaseHome};MVCC=true"
  user = "sa"
  password = "sa"
}

このファイルを使用するデータベースにあわせて以下のように編集します(URLやユーザ名、パスワードは使用する環境にあわせて適宜変更してください)。データベースはあらかじめ作成しておく必要があります。なお、データベースは使用する文字コードで作成しておく必要があります。

MySQLの場合

db {
  url = "jdbc:mysql://localhost/gitbucket?useUnicode=true&characterEncoding=utf8"
  user = "test"
  password = "test"
}

MySQLの場合は5.7以上である必要があるので注意してください。

PostgreSQL

db {
  url = "jdbc:postgresql://localhost/gitbucket"
  user = "test"
  password = "test"
}

これでGitBucketを再起動すればMySQLまたはPostgreSQLの指定したデータベースに必要なテーブルが自動的に作成され、外部データベースでGitBucketを使い始めることができます。

アップグレードの場合

既存のGitBucketを外部データベースを使用するように設定する場合は一度データをエクスポートしておき、データベースの設定後にインポートしなおす必要があります。また、GitBucket 3系から4系にアップグレードする場合、以下の2点に注意が必要です。

  • まず一度GitBucket 3系の最終バージョンである3.14までアップデートしてから4系にアップデートする必要がある
  • GitBucket 4系にアップデートする際は一度プラグインをアンインストール(~/.gitbucket/pluginsディレクトリからjarファイルを削除)しておく必要がある

データのエクスポート

GitBucketを4系にアップグレードするとGitBucketの管理コンソールから以下のように既存のH2内のデータをエクスポートする機能が利用可能になりますので、ここでデータをXMLまたはSQLでエクスポートしておきます。

f:id:takezoe:20160716021017p:plain

XMLだとインポート時にGitBucketの管理コンソールから作業が可能ですが、SQLだと自分でDBに直接インポートする必要があります。そのためSQLのほうが作業は面倒なのですが、データが大きい場合などXMLだとインポートに失敗することがあるので、可能であればXMLSQL両方ともエクスポートしておくとよいでしょう。

プラグインのデータも移行する場合はプラグインのテーブル(gitbucket-gist-pluginであればGISTテーブルとGIST_COMMENTテーブル)も選択しておきます。プラグインのデータが不要な場合はチェックを外しておきます。いったん基本データだけ移行してプラグインのデータは後から移行したいという場合は二回に分けてエクスポートしておきます。

データのインポート

エクスポートに成功したら一度GitBucketを停止し、使用するデータベースにあわせて~/.gitbucketディレクトリにあるdatabase.confを前述の通り修正します。また、プラグインのデータも移行する場合はGitBucket4系に対応したバージョンのプラグインをインストールしておきます。

この状態でGitBucketを起動するとクリーンインストール状態で立ち上がりますのでデフォルトの管理ユーザであるroot / rootでログインし、管理コンソールからさきほどエクスポートしたXMLファイルをインポートします。インポートが成功すれば外部データベースへの移行は完了です。

ただし、PostgreSQLの場合のみ、追加で以下のSQLを実行しておく必要がありますのでpsqlコマンドなどを使って実行しておいてください。

SELECT setval('label_label_id_seq', (select max(label_id) + 1 from label));
SELECT setval('activity_activity_id_seq', (select max(activity_id) + 1 from activity));
SELECT setval('access_token_access_token_id_seq', (select max(access_token_id) + 1 from access_token));
SELECT setval('commit_comment_comment_id_seq', (select max(comment_id) + 1 from commit_comment));
SELECT setval('commit_status_commit_status_id_seq', (select max(commit_status_id) + 1 from commit_status));
SELECT setval('milestone_milestone_id_seq', (select max(milestone_id) + 1 from milestone));
SELECT setval('issue_comment_comment_id_seq', (select max(comment_id) + 1 from issue_comment));
SELECT setval('ssh_key_ssh_key_id_seq', (select max(ssh_key_id) + 1 from ssh_key));

XMLでのインポートに失敗する場合はエクスポートしたSQLファイルを直接DBにインポイートします。たとえばMySQLの場合は以下のようにします。

$ mysql -u root -p gitbucket < gitbucket-export-xxxxxxxx.sql

まとめ

GitBucketを外部データベースで動かす方法について紹介しました。デフォルトのH2はデータが破損することがあるというレポートを多くのユーザさんから頂いていますので、繰り返しになりますができるだけMySQLPostgreSQLを利用されることをおすすめします。

既存のGitBucketのデータを外部データベースに移行する場合は多少複雑な手順が必要になりますが、もしトラブルに遭遇した場合はGitterでご相談いただければと思います。

gitter.im

JekyllでTwitterカード用のタグを出力する

前回紹介したjekyll-seo-tagプラグインはogp用のタグは出力してくれるのですが、twitterカード用のタグを出力するにはTwitterのユーザ名を設定しないといけないようです。

GitBucketは自分のTwitterアカウントで情報を流していますが、プロジェクトとしてのアカウントはまだありませんし、Twitterカード自体はtwitter:siteを出力しなくても動作する様子です。グーグル先生に聞いてみたところ以下のような記事を見つけたので、これとjekyll-seo-tagプラグインを参考にしてtwitterカード用のタグをいい感じに出力するテンプレートを作ってみました。

davidensinger.com

{% assign twitter_description = page.description | default: page.excerpt | default: site.description %}
{% if twitter_description %}
  {% assign twitter_description = twitter_description | markdownify | strip_html | strip_newlines | escape_once %}
{% endif %}
{% if page.title %}
  {% assign twitter_title = page.title | append: " - " | append: site.title %}
{% else %}
  {% assign twitter_title = site.title %}
{% endif %}
{% if page.image %}
  {% assign twitter_image = site.baseurl | append: page.image %}
  {% assign twitter_card = "summary_large_image" %}
{% else %}
  {% assign twitter_image = site.baseurl | append: "/favicon.png" %}
  {% assign twitter_card = "summary" %}
{% endif %}
<meta name="twitter:card" content="{{ twitter_card }}">
<meta name="twitter:title" content="{{ twitter_title }}">
<meta name="twitter:url" content="{{ site.baseurl }}{{ page.url }}">
<meta name="twitter:description" content="{{ twitter_description }}">
<meta name="twitter:image:src" content="{{ twitter_image }}">

これを_includes/head.htmlなどに入れておけばOKです。画像ファイルはjekyll-seo-tagプラグインと同じく記事ごとにヘッダのYAML部分のimage属性に指定する必要があります。

これで記事のURLをツイートすると以下のような感じで表示されるようになります。

jekyll-seo-tagプラグインのソースを見てるとtwitter:cardを出力するだけでもいいっぽい(ogp用の該当する情報を自動的に拾ってくれる?)のですが、自分で試してみたところ、TwitterAndroidアプリでは表示されるけどWebでは表示されなかったりしたのでとりあえずogpとTwitterカード用のmetaタグを両方出力するようにしています。

GitHub PagesのJekyllでogpタグを出力する

GitHub Pagesのドキュメントによるとjekyll-seo-tagというプラグインが使えるみたいです。

github.com

まず_config.ymlに以下の記述を追加します。

gems:
  - jekyll-seo-tag

それから_layouts/_default.htmlなどタグを出力したい位置に{% seo %}という記述を追加します。

<html>
  <head>
    {% include head.html %}
    {% seo %}
  </head>
</html>

とりあえずこれだけでサイトの設定や記事のデータに応じて必要最低限のタグは出力されますが、各ページや記事のヘッダ部分のYAMLにimage属性を記述しておくとog:imageタグを出力できます。

---
layout: post
title: "GitBucket 4.2 released!"
date: 2016-07-02 00:00:00
image: /images/gitbucket-4.2/adminlte.png
categories: gitbucket
---

他にもいろんな設定があるみたいなのでjekyll-seo-tagのREADMEを読んでおくとよいでしょう。

ちなみにjekyll-auto-imageプラグインを使うと記事中の画像ファイルのパスを自動的に取得できるのでogpやtwitterカードなどのタグを簡単に生成できそうですが、残念ながらGitHub Pagesでは使えないみたいです。

github.com

GitBucket 4.2をリリースしました

GitBucketを3.x系から4.0にアップデートしていた場合に4.2にアップデートできないというバグがあることが発覚したためこれを修正した4.2.1をリリース済みです。この条件に該当する方は4.2ではなく4.2.1を使用するようにしてください(ただし、新規にGitBucketをインストールする場合は4.2をご利用下さい)。

https://github.com/gitbucket/gitbucket/releases/tag/4.2.1

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

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

AdminLTEの適用によるUIの刷新

GitBucketはGitHubライクなUIから独自UIへの移行を進めていますが、このバージョンではBootstrap用の管理画面テーマであるAdminLTEを適用しました。

github.com

ヘッダやサイドバーが固定され、より使いやすく洗練されたUIになっています。UIの改善は今後も継続的に行っていく予定です。

f:id:takezoe:20160626021014p:plain

gitリポジトリgc

リポジトリ管理画面のDanger Zoneでgitリポジトリgcを行えるようになりました。

f:id:takezoe:20160626021023p:plain

WikiとIssuesを無効化可能に

リポジトリの設定画面でWikiとIssuesを無効にできるようになりました。

f:id:takezoe:20160626021035p:plain

もし外部のWikiサービスやイシュートラッキングサービスを使用している場合、WikiやIssuesを無効にした上で外部サービスのURLを入力しておくことでサイドメニューから外部サービスにリンクすることができます。

SMTP設定のテストメール

SMTPの設定が正しいことを確認するために指定したURLにテストメールを送信できるようになりました。

f:id:takezoe:20160626021048p:plain

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

Netflixのオープンソースソフトウェア

ここのところHystrixについて調べていたのですが、Netflixは他にもGitHub上で様々なOSSを公開しています。

github.com

Javaのものが中心ですがPythonやGo、Cで書かれているものもあります。ライブラリ的なものからミドルウェアや運用ツールまで多岐に渡っており、NetflixAWSを利用しているということもありAWS上での利用に特化したものもあります。また各プロダクトのドキュメントもしっかりしており、以下のような専用のサイトも立ち上げられており、社内で開発したものを積極的にOSS化するという方針が伺えます。

netflix.github.io

HystrixやEurekaなどを筆頭に有名なものも多いのですが、なにぶん数が多くどのようなものがあるのかを把握するのも割と一苦労な感じなのですが、Netflixでは自社のOSSを紹介するMeetupが継続的に開催されているらしく、そのスライドを集めてみました。

www.slideshare.net

www.slideshare.net

www.slideshare.net

www.slideshare.net

www.slideshare.net

www.slideshare.net

www.slideshare.net

2014年のスライドですが、初期の頃からあるプロダクトについては以下が参考になります。

www.slideshare.net

日本語では以下の記事でまとめられています。

d.hatena.ne.jp

wazanova.jp

HystrixをScala / Playアプリケーションから使ってみる

前回はHystrixの簡単な紹介を書きました。

takezoe.hatenablog.com

HystrixはJavaライブラリなのでもちろんScalaからも使うことができるのですが、そのままだと若干Scalaからは使いにくい部分もあります。今回はScala(主にPlay Framework)でHystrixを使う場合について書いてみたいと思います。

Playにモニタリング用のエンドポイントを追加する

HystrixをScalaで使うには普通にHystrixCommandをScalaで実装してそれを呼び出せばよいのですが、問題はダッシュボードからのモニタリング用のエンドポイントが標準ではサーブレットコンテナ用のものしか用意されていないということです。

探してみたところ、PlayにHystrixを組み込むサンプルを作っている方がいました。

github.com

上記のリポジトリにあるHystrixSupportというコントローラがモニタリング用のエンドポイントの実装になります。ただ、このサンプルはPlayのバージョンが古く(2.3 系)、最新のPlay 2.5ではそのままでは動かなかったり警告が出たりします。そこでこのHystrixSupportをPlay 2.5用に修正してみました。

package controllers

import play.api.mvc._
import java.util.concurrent.atomic.{AtomicInteger, AtomicReference}
import com.netflix.config.scala.DynamicIntProperty
import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsPoller
import play.api.Logger
import play.api.libs.iteratee.Enumerator
import java.util.concurrent.TimeUnit
import scala.annotation.tailrec
import scala.concurrent.duration.FiniteDuration
import akka.actor.ActorSystem
import java.io.OutputStream
import javax.inject.Inject
import akka.stream.scaladsl.Source
import play.api.libs.streams.Streams
import scala.util.Try

class HystrixSupport @Inject()(system: ActorSystem) extends Controller {
  import play.api.libs.concurrent.Execution.Implicits.defaultContext

  def stream(delayOpt: Option[Int]) = Action {

    val numberConnections = concurrentConnections.incrementAndGet()
    val maxConnections = maxConcurrentConnections.get

    Some(numberConnections).
      filter(_ <= maxConnections).
      map(_ => delayOpt.getOrElse(500)).
      fold(unavailable(maxConnections)) { delay =>
        val source = Source.fromPublisher(Streams.enumeratorToPublisher(streamRequest(delay)))
        Ok.chunked(source).withHeaders(
          "Content-Type" -> "text/event-stream;charset=UTF-8",
          "Cache-Control" -> "no-cache, no-store, max-age=0, must-revalidate",
          "Pragma" -> "no-cache"
        )
      }
  }

  private[this] def unavailable(max: Int) = {
    concurrentConnections.decrementAndGet()
    ServiceUnavailable(s"MaxConcurrentConnections reached: $maxConcurrentConnections")
  }

  private[this] final val concurrentConnections: AtomicInteger = new AtomicInteger(0)
  private[this] final val maxConcurrentConnections: DynamicIntProperty =
    new DynamicIntProperty("hystrix.stream.maxConcurrentConnections", 5)

  private[this] def streamRequest(delay: Int): Enumerator[Array[Byte]] = {
    val listener = new MetricJsonListener(1000)
    val poller = new HystrixMetricsPoller(listener, delay)
    poller.start()
    Logger.info("Starting poller")

    val delayDuration = FiniteDuration(delay, TimeUnit.MILLISECONDS)
    //val system = Akka.system

    val streamer = (out: OutputStream) => produceStream(poller, listener, delayDuration, system, out)
    val closer = () => {
      Logger.info("Closing poller")
      poller.shutdown()
      concurrentConnections.decrementAndGet()
    }

    val enum = Enumerator.outputStream(streamer)
    enum.onDoneEnumerating(closer())
  }

  private[this] def produceStream(poller: HystrixMetricsPoller, listener: MetricJsonListener, delay: FiniteDuration, system: ActorSystem, out: OutputStream): Unit = {
    val strings = produce(poller, listener)
    if (strings.isEmpty) {
      out.flush()
      out.close()
    }
    else {
      strings.foreach(s => out.write(s"$s\n\n".getBytes("UTF-8")))
      out.flush()
      Try(system.scheduler.scheduleOnce(delay)(produceStream(poller, listener, delay, system, out)))
    }
  }

  private[this] def produce(poller: HystrixMetricsPoller, listener: MetricJsonListener): Vector[String] = {
    if (!poller.isRunning) Vector()
    else {
      val jsonMessages = listener.getJsonMetrics

      if (jsonMessages.isEmpty) Vector("ping: ")
      else jsonMessages.map(j => s"data: $j")
    }
  }

  private class MetricJsonListener(capacity: Int) extends HystrixMetricsPoller.MetricsAsJsonPollerListener {

    private[this] final val metrics = new AtomicReference[Vector[String]](Vector())

    @tailrec
    private[this] final def set(oldValue: Vector[String], newValue: Vector[String]): Boolean = {
      metrics.compareAndSet(oldValue, newValue) || set(oldValue, newValue)
    }

    private[this] final def getAndSet(newValue: Vector[String]): Vector[String] = {
      val oldValue = metrics.get
      set(oldValue, newValue)
      oldValue
    }

    def handleJsonMetric(json: String): Unit = {
      val oldMetrics = metrics.get()
      if (oldMetrics.size >= capacity) throw new IllegalStateException("Queue full")

      val newMetrics = oldMetrics :+ json
      set(oldMetrics, newMetrics)
    }

    def getJsonMetrics: Vector[String] = getAndSet(Vector())
  }
}

Playアプリケーションに組み込むにはまずbuild.sbtに以下の依存関係を追加します。

libraryDependencies ++= Seq(
  "com.netflix.archaius"  % "archaius-scala"                  % "0.7.4",
  "com.netflix.hystrix"   % "hystrix-core"                    % "1.5.3",
  "com.netflix.hystrix"   % "hystrix-metrics-event-stream"    % "1.5.3"
)

それからroutesに以下のルーティングを追加します。

GET    /hystrix.stream    controllers.HystrixSupport.stream(delay: Option[Int])

このエンドポイントをダッシュボードでモニタリングすればOKです。

HystrixのコマンドをFutureに変換する

上記のPlayでのサンプルの中にHystrixのコマンドをScalaのFutureに変換するためのimplicit classがあるのですが、これを少し改変してみました。

package util

import scala.concurrent.{Future, Promise}
import com.netflix.hystrix.HystrixObservable

object Futures {

  private class ForPromiseObserver[T](p: Promise[T]) extends rx.Observer[T] {
    def onNext(t: T): Unit = p.trySuccess(t)
    def onError(e: Throwable): Unit = p.tryFailure(e)
    def onCompleted(): Unit = ()
  }

  implicit final class HystrixCommandWithScalaFuture[T](val cmd: HystrixObservable[T]) extends AnyVal {
    def future: Future[T] = {
      val promise = Promise[T]()
      val observer = new ForPromiseObserver(promise)

      cmd.observe().subscribe(observer)

      promise.future
    }
  }
}

以下のような感じでfutureメソッドでコマンドをFutureに変換できます。

import utils.Futures._

class Application() extends Controller {
  def index = Action.async {
    new CommandHelloWorld("World").future.map(Ok(_))
  }
}

FutureをHystrixのコマンドに変換する

同期的な処理をScalaFutureに合成したい場合は前述のimplicit classでよいのですが、もともとFutureを返すScalaライブラリにHystrixを適用したい場合はもう少し工夫が必要になります。以下のようなアダプタを作ってみました。

package utils

import com.netflix.hystrix.{HystrixCommandGroupKey, HystrixObservableCommand}
import rx.Observable
import rx.lang.scala.subjects.ReplaySubject
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}

abstract class HystrixFutureCommand[T](groupKey: HystrixCommandGroupKey)(implicit ec: ExecutionContext) extends HystrixObservableCommand[T](groupKey) {

  override def construct(): Observable[T] = {
    val channel = ReplaySubject[T]()

    run().onComplete {
      case Success(v) => {
        channel.onNext(v)
        channel.onCompleted()
      }
      case Failure(t) => {
        channel.onError(t)
        channel.onCompleted()
      }
    }

    channel.asJavaSubject
  }

  def run(): Future[T]

}

こんな感じで使います。

class FutureHelloCommand extends HystrixFutureCommand[String](HystrixCommandGroupKey.Factory.asKey("HelloWorldAsync")){

  def run(): Future[String] ={
    Future {
      "Hello World!"
    }
  }

  override protected def resumeWithFallback(): Observable[String] = {
    Observable.from(Array("resume!"))
  }
}

エラー処理を行う場合、Future#recover()でフォールバック処理を行ってしまうとHystrixでエラーのハンドリングができないので、コマンド側のフォールバック機構を使用する必要があるという点に注意が必要です。

まとめ

HystrixはJava用のライブラリですが、簡単なアダプタを用意することでScalaでもさほど違和感なく使用することができます。

ただ、使用するフレームワークによってはモニタリング用のエンドポイントを自前で実装しないといけないのでちょっと大変かもしれません。また、ScalaFutureを使っている場合、スレッドプールの管理やエラーハンドリングなどHystrixの機能と被る部分があるため効率が悪かったり方針の整理が必要になりそうです。ScalaFutureとHystrixをシームレスに統合できる方法を考えられるともっと使いやすくなりそうです。

なお、今回Play 2.5用に修正したり新たに追加したものについては整理して冒頭で紹介したPlay + Hystrixのサンプルプロジェクトにプルリクエストしておきました。

github.com