Ratpack入門 (番外編) - Sentryを使う

## スレッドベース vs イベント駆動

Ratpackはノンブロッキング・イベント駆動モデルで動作します。
データベースアクセスのようなブロッキング操作は、別スレッドで行わねばなりません。手書きでそれをやるのは大変ですから、Ratpackには`Promise`によるコールバックの仕組みと、`Blocking`ユーティリティーによるマルチスレッディングのサポートがついています。
これだけなら「ああ、JavaScriptみたいな感じね」で済む話なのですが、他のフレームワークを併用するとき、少々問題が発生します。

Javaの標準的なWebフレームワークであるJavaサーブレットは、1リクエスト-1スレッドのモデルで動作します。Spring Frameworkもサーブレットベースなので、このモデルに準拠します。サーブレットはHTTPリクエストを受けるとき、新しくスレッドを作成し(もしくはプールから取得し)、ハンドラーメソッドを実行します。この仕組みのおかげで各リクエストはそれぞれ別のスレッド内で実行されるため、プログラマーは(基本的には)スレッド間のリソースの共有などスレッドセーフティーの心配することなく、プログラムを書くことができます。
ところがRatpackはノンブロッキングなフレームワークなので、全てのリクエストが同じイベントループ、同じスレッドの中で実行されます(正確にはちょっと違いますが大体のイメージはそんな感じです)。ブロッキング処理は別スレッドで実行されるので、一つのリクエストが一つのスレッド内で完結するとは限りません。大抵のWebアプリケーションはデータベースに接続したり、外部のAPIを呼び出したりすると思います。
まとめると、サーブレットとRatpackでは、実行の「コンテキスト」とスレッドの関係が対応しないということです。

それ自体は別に設計思想の違いでありどちらが良い悪いという話ではないのですが、Javaの事実上の標準であるサーブレットが1リクエスト-1スレッドモデルなので、多くのJavaフレームワークはそれを前提に作られています。そのため、Ratpackとそのままでは一緒に使うことができないという事態が発生します。


## `ThreadLocal`

(`ThreadLocal`なんて常識だという人は次のパラグラフへ!)

代表的な例が[`ThreadLocal`](https://docs.oracle.com/javase/jp/8/docs/api/index.html?java/lang/ThreadLocal.html)です。スレッドローカルは各スレッドに固有のインスタンスを保持するためのクラスです。例として[`ThreadLocalRandom`](https://docs.oracle.com/javase/jp/8/docs/api/index.html?java/util/concurrent/ThreadLocalRandom.html)を考えてみましょう。
実はJavaの[`Random`](https://docs.oracle.com/javase/jp/8/docs/api/java/util/Random.html)クラスはスレッドセーフです。複数のスレッドで共有しても安全です。ただし同期されるため、競合する場合パフォーマンスが悪くなります。
ここで`ThreadLocalRandom`が活躍します。スレッドAとBが同じ`ThreadLocalRandom`に同時にアクセスしたとき、スレッドAはスレッドAに固有の内部的な`Random`を、スレッドBはそれとは別の、スレッドB専用の`Random`を使用します。同期が発生しないので、パフォーマンスは悪化しません。また、その場で毎回新しい`Random`インスタンスを作るよりも効率的です。

`ThreadLocal`を使えば、同期の心配をすることなく、マルチスレッドに対応していないクラスを使用できるのです。


## コンテキストの危機

最初に述べたように、サーブレットは1リクエスト-1スレッドで動作します。`ThreadLocal`は各スレッドに固有ですから、実際には「`ThreadLocal`はリクエストごとに一つのインスタンスを持つ」とみなすことができます。これはWebアプリケーションの実行コンテキストを管理するのに、とても便利な設計でした。例えばユーザー情報をスレッドローカルに保存すれば、**プログラムはそのスレッドローカルなユーザー情報を、現在のリクエストの情報とみなすことができる**のです。

代表例が[logback](https://logback.qos.ch/)の[MDC](https://logback.qos.ch/manual/mdc_ja.html)です。最初にリクエストを受け付けたときにMDCにユーザー情報を保存しておけば、以後のログ出力で、そのユーザー情報を一緒に利用できるようになります。ログ出力側は、自分を呼び出しているのがどのリクエストなのか意識しないでも、その情報を利用できるのです。便利ですね。

お気づきでしょうか、Ratpackはノンブロッキングです。一つのリクエストが、一つのスレッドの中で実行されるわけではありません。**`ThreadLocal`はRatpack上では使えません。**

具体例でみてみましょう。

```java
MDC.put("user", "John Smith");
logger.info("[1]")
Blocking.op(() -> {
    logger.info("[2]");
}).then(() -> {
    logger.info("[3]")
    ctx.render( "OK" );
});
```

サーブレットであれば、3つのログ出力はどれも同一のMDCを参照します。すべてのログ出力で、MDCの`user`の値は`John Smith`になります。
Ratpackでは、`Blocking.op()`内の処理は別スレッドです。`MDC.put()`を呼び出したスレッドとは別スレッドです。`[2]`の`user`は`null`になってしまいます。


## `Execution`

では、サーブレットで行っていたようなコンテキストの管理を、Ratpackではどのように行えばよいのでしょうか?

実は、Ratpackには処理の実行を[`Execution`](https://ratpack.io/manual/current/api/ratpack/exec/Execution.html)という単位で行います。Ratpackの`Promise`がRatpackアプリケーションの内部でしか使えないというルールは、実際には「`Execution`の中でしか使えない」ということだったのです。
`Execution`は処理のコンテキストでもあります。スレッドがサーブレットにおける実行コンテキストであるように、Ratpackアプリケーションは`Execution`においてコンテキストを管理します。`Execution`にはその内部を通して利用できる辞書機能を持っており、ある型のインスタンスを保存したり取り出したりすることができます。これを`ThreadLocal`の代わりに活用できます。

`ThreadLocal`はRatpackでは使えないと述べましたが、RatpackにはMDCを利用するための[`MDCInterceptor`](https://ratpack.io/manual/current/api/ratpack/logging/MDCInterceptor.html)があらかじめ用意されています。使い方は簡単で、[`Registry`](https://ratpack.io/manual/current/api/ratpack/registry/Registry.html)にこのクラスを登録するだけです。
`MDCInterceptor`は、[`ExecInterceptor`](https://ratpack.io/manual/current/api/ratpack/exec/ExecInterceptor.html)インターフェースの実装です。このインターフェースは名前の通り、`Execution`に対するインターセプターとして動作します。すなわち、ある`Execution`の実行前後に、何らかの処理を挟むために使用するものです。

`MDCInterceptor`の実装をのぞいてみると、処理はおおまかに次のような流れになっていることがわかります。

1. `Execution`からコンテキストの辞書を取り出す。ない場合、空のコンテキストの辞書を作成する。
2. `MDC.setContextMap()`を呼び、取り出した辞書を設定する。
3. `Execution`を実行する。
4. MDCからコンテキストの辞書を取り出し(`MDC.getCopyOfContextMap()`)、`Execution`に設定

要するに、実際の処理の前にSLF4JのMDCを操作して、Ratpackの`Execution`とMDCが一致するように操作しています。


## Sentryを使う

ようやく本題に入ります。ログ監視サービス[Sentry](https://sentry.io)のJavaクライアントも、Ratpackにおいては若干の操作が必要になります。

SentryにはTagやBreadcrumbのような、実行のコンテキストを記録して出力する機能があります。しかし、[Sentryのドキュメントにも書かれているように](https://docs.sentry.io/clients/java/context/)、「コンテキスト」が何を意味するかはアプリケーションによって異なります。Sentryは、このコンテキストの管理を`ContextManager`というクラスで抽象化しています。デフォルトでは二つの実装が用意されており、Android用Javaクライアントは`SingletonContextManager`、通常版では`ThreadLocalContextManager`が使用されます。名前の通りシングルトンとスレッドローカルです。そして、今まで議論してきたように、スレッドローカルはRatpackでは使えません。シングルトン実装ではユーザー情報がおかしくなってしまいます。

そこで、`Execution`を利用した独自の実装の`ContextManager`を書くことにします。

```java
@Override
public Context getContext() {
    Optional<Execution> currentExecOpt = Execution.currentOpt();

    if ( currentExecOpt.isPresent() ) {
        Execution currentExec = currentExecOpt.get();
        Optional<Context> context = currentExec.maybeGet( Context.class );

        if ( context.isPresent() ) {
            return context.get();
        } else {
            Context newContext = new Context();
            currentExec.add( newContext );
            return newContext;
        }
    } else {
        return singletonContext;
    }
}
```

まず、現在のプログラムが`Execution`内で実行されていることを確かめます。`Execution`の外にいる場合、シングルトンの`Context`オブジェクトを返します。これは別にスレッドローカルなどでもいいのですが、`Execution`の外で実行されるコードは、自前でスレッドを作成したりしない限り、サーバーの起動処理くらいだと思います。`Context`自体は`synchronized`されておりスレッドセーフなので、このような実装にすることにしました。

`Execution`内にいた場合、現在の`Execution`にSentryの`Context`オブジェクトが保存されているかを確かめます。保存されていない場合は新しく`Context`を作成し、`Execution`に登録します。これで以後の`Execution`にも`Context`が引き継がれるようになります。`Blocking`などで別のスレッドに移動したとしても、同じ`Context`が取得できるようになりました。

あとはこの`ContextManager`実装を使用するようにすればいいのですが、これは、`SentryClientFactory`をオーバーライドすることで実現できます。

```java
public final class RatpackSentryClientFactory extends DefaultSentryClientFactory {
    @Override
    protected ContextManager getContextManager( Dsn dsn ) {
        return new RatpackSentryContextManager();
    }
}
```

これだけです。

最後に、Sentryクライアントを作成するデフォルトの方法として、このクラスを指定してあげる必要があります。やり方としては、`Sentry.init()`メソッドを使用する方法と、プロパティーを使う方法があります。

```java
Sentry.init( new RatpackSentryClientFactory() );
```

ただ、`sentry-logback`のような他のロギングフレームワークと統合されたクライアントを使う場合、Sentryの初期化はグローバルな`Sentry`クラスに依存していると思います。実際、`sentry-logback`は個別の`SentryClient`を指定するような作りにはなっていません。そのため、プロパティーファイル経由で設定するほうが好ましいと思います。

```java:sentry.properties
factory=factory=rip.deadcode.ratpack.sentry.RatpackSentryClientFactory
```

`factory`キーの値に、作成した`SentryClientFactory`のFQCNを指定します。


## まとめ

* Ratpackで`ThreadLocal`には気を付けよう
* [ソースコード](https://github.com/minebreaker-tf/ratpack-sentry)