Scientist gemについて調べた

# About [scientist](https://github.com/github/scientist)

scientist gemはgithub社が作ったテストツール(=新ロジックの実験ができるツール)です。

> Let's pretend you're changing the way you handle permissions in a large web app. Tests can help guide your refactoring, but you really want to compare the current and refactored behaviors under load.

気持ちとしては

> テストはリファクタリングした結果、動作に問題ないことを認識するときに非常に役立つが、本当に現行とリファクタ後の振る舞いが変っていないか比較することはできない。

痛いほど分かる。

### なぜに？

February 03, 2016にScientist 1.0 リリースに伴うポスト
http://githubengineering.com/scientist/

大規模リファクタするためには[Branch by Abstraction](http://martinfowler.com/bliki/BranchByAbstraction.html) いいよね。でも、Abstraction layerいれると、code pathが本来と異なるのでrefactor後とその前の振る舞いを試験する方法として固くない。
- 例えば、予想外のところから呼ばれるとabstraction layer通らないじゃん？

![](http://martinfowler.com/bliki/images/branch-by-abstraction/step-4.png)

テストがなんで十分じゃないか
- 十分に複雑なシステムではテストケース網羅すること出来ないよね。網羅しようとするとテスト量が多くなって開発速度メッチャ落ちる
- そもそも既存のデータ品質に問題があるかもしれない。そんなデータにぶつかったときの振る舞いなんてわからない。

だからproduction dataとUserで試験が必要なんだ。

### 特徴

code pathを変えないでproduction dataでの振る舞いを試験できる。

新機能の試験するとしたらこんな感じがハッピー:

![](http://githubengineering.com/images/scientist/scientist-flow-diagram.png)

# Getting Started

Sample Code: https://github.com/threetreeslight/rails-lab/tree/scientist

以下の関係を持つRailsアプリをサンプルに解説します

```
Author 1-* Book
```

また、scientistを試すmethodは以下に成ります。

```ruby
class Author < ApplicationRecord
  def luckey_number
    id
  end
end
```

### old wayとnew wayの比較の仕組みづくり

old way(既存実装)とnew way(リファクタ後の実装)

```ruby
class Author < ApplicationRecord
  def luckey_number
    experiment = Scientist::Default.new "book.lucky_number"
    experiment.context author: id, name: name

    experiment.use { id } # old way
    experiment.try { rand(10) } # new way

    experiment.run
  end
end
```

luckey_numberでは、use blockとtry blockの両方が実行されます。
こうすることで、新実装との返り値の違いを検証することが可能です。

また、experimentは必ずuse blockの実行結果がreturnされます。

### try実行の制御や配信

`Scientist::Default`は、tryの実行制御やcompare結果の配信を行います。

`Scientist::Default` classは `Scientist::Experiment`用のInterfaceの仮実装になるので、実際には何も制御・送信されません。
https://github.com/github/scientist/blob/master/lib/scientist/default.rb

そのため、専用のexperimentを作ります。

```ruby
class MyExperiment < Scientist::Default
  def publish(result)
    # Store the timing for the control value,
    StatsD.measure "science.#{name}.control", result.control.duration
    # for the candidate (only the first, see "Breaking the rules" below,
    StatsD.measure "science.#{name}.candidate", result.candidates.first.duration

    # and counts for match/ignore/mismatch:
    if result.matched?
      StatsD.increment "science.#{name}.matched"
    elsif result.ignored?
      StatsD.increment "science.#{name}.ignored"
    else
      StatsD.increment "science.#{name}.mismatched"
      # Finally, store mismatches in redis so they can be retrieved and examined
      # later on, for debugging and research.
      store_mismatch_data(result)
    end
  end

  def store_mismatch_data(result)
    payload = {
      :name            => name,
      :context         => context,
      :control         => observation_payload(result.control),
      :candidate       => observation_payload(result.candidates.first),
      :execution_order => result.observations.map(&:name)
    }

    key = "science.#{name}.mismatch"

    Rails.logger.warn "#{key}: #{payload}"

    REDIS.lpush key, payload
    REDIS.ltrim key, 0, 1000
  end

  def observation_payload(observation)
    if observation.raised?
      {
        :exception => observation.exception.class,
        :message   => observation.exception.message,
        :backtrace => observation.exception.backtrace
      }
    else
      {
        # see "Keeping it clean" below
        :value => observation.cleaned_value
      }
    end
  end
end
```

ここでは以下の内容を計測しています。

- 実行時間をgraphiteに送り込む
- mismatch, matchの発生回数をgraphiteに送り込む
- mismatchが発生したときに、その細かい情報をredisに蓄積する
- mismatchが発生したときに、その細かい情報をlogに吐き出す

#### Redis

```
$ vim config/initialize/redis.rb

require 'redis'

REDIS = Redis.new(url: ENV['REDIS_URL'])
```

#### metric

Github社のSampleではmetricをgraphiteに放り込んでいるそうです。

metricの受け口は[statsd](https://github.com/etsy/statsd)互換の
[brubeck](https://github.com/github/brubeck)を利用しているそうですが、今回はstatsdでいきます。

設定が面倒なので、graphiteとstatsdの設定が面倒なのでこのイメージを使いました。
https://github.com/hopsoft/docker-graphite-statsd

rails -> statusDへの流し込みは、[Shopify/statsd-instrument](https://github.com/Shopify/statsd-instrument)を使いました。

```
$ vim config/initialize/status_d.rb

require 'statsd-instrument'

# Sets up a UDP backend. First argument is the UDP address to send StatsD packets to,
# second argument specifies the protocol variant (i.e. `:statsd`, `:statsite`, or `:datadog`).
StatsD.backend = StatsD::Instrument::Backends::UDPBackend.new(ENV['STATSD_URL'], :statsite)
```

ref graphiteの良さ [Mackerelを支える時系列データベース技術](http://blog.yuuk.io/entry/high-performance-graphite)

### 作ったExperimentの適用

```diff
class Author < ApplicationRecord
  has_many :books

  def luckey_number
+   experiment = Scientist::Experiment.new "book.lucky_number"
    experiment.context author: id, name: name

    experiment.use { id } # old way
    experiment.try { rand(10) } # new way

    experiment.run
  end
end

+ module Scientist::Experiment
+  def self.new(name)
+     MyExperiment.new(name)
+   end
+ end
```

### 実行


```
$ docker-compose up --build -d app

$ docker-compose exec app bash
> rails c
> 100.times{ Author.first.luckey_number }
  Author Load (2.5ms)  SELECT  "authors".* FROM "authors" ORDER BY "authors"."id" ASC LIMIT ?  [["LIMIT", 1]]
{:name=>{:name=>"book.lucky_number"}, :context=>{:author=>1, :name=>"Chuck norris"}, :control=>{:value=>1}, :candidate=>{:value=>6},:execution_order=>["control", "candidate"]}
=> 1
```

### 実行metric

```
$ open http://`dinghy ip`:8080/dashboard
```

がんばるとgithubのblogにあるようなこんなのがみれる

![](http://githubengineering.com/images/scientist/pullable-by-graph-total-mismatches.png)

### 細かい失敗データ on redis

```
$ docker-compose exec app bash
> radis-cli -h redis

redis:6379> KEYS science.*
1) "science.book.lucky_number.mismatch"
redis:6379> LRANGE science.book.lucky_number.mismatch 0 -1
1) "{:name=>\"book.lucky_number\", :context=>{:author=>1, :name=>\"Chuck norris\"}, :control=>{:value=>1}, :candidate=>{:value=>8}, :execution_order=>[\"control\", \"candidate\"]}"
2) "{:name=>\"book.lucky_number\", :context=>{:author=>1, :name=>\"Chuck norris\"}, :control=>{:value=>1}, :candidate=>{:value=>7}, :execution_order=>[\"candidate\", \"control\"]}"
3) "{:name=>\"book.lucky_number\", :context=>{:author=>1, :name=>\"Chuck norris\"}, :control=>{:value=>1}, :candidate=>{:value=>0}, :execution_order=>[\"candidate\", \"control\"]}"
```

# 使い所

個人的に考える使い所は以下です。

- 影響範囲が大きすぎてテストしきれないようなところ
- 変更が多く積み重なって、過去のデータも含め整合性が保てているか心配なところ

この手のものはものは本番で踏み荒らすしかないよねってほんと思う。

更に

- 特定のユーザーだけtry項を実行させる
- 特定の確立でtry項を実行させる

など新ロジックの実行を制御することも可能。使い勝手良い。

# コードの流れ

```
├── scientist
│   ├── default.rb
│   ├── errors.rb
│   ├── experiment.rb
│   ├── observation.rb
│   ├── result.rb
│   └── version.rb
└── scientist.rb
```

line数は大したことはない。
動きとしてもとてもシンプルです。

1. Scientist::Experiment interfaceを実装した実験実行計画( defaultだと `Scientist::Default`)を作成
2. Scientist::Experiment.run にて実行し、比較結果や例外情報などを `Scientist::Result` に格納
3. 1で作成した実行計画に設定された情報に乗っ取り、`Scientist::Result`の情報を配信