読者です 読者をやめる 読者になる 読者になる

ンンンパ

1年間無職だった

Go 言語でつくったもののメモリとかをなんとなく見れるようにする

golang 日記

pprof を用いた詳細な情報を得る方法はさんざん紹介されており、しかしその詳細の情報のどこを見ればいいのかわからないので、とにかく簡単に見れるような施策を打ってみました。

f:id:mmmpa:20161202202126g:plain

これは社の Slack で動いてるボットの一つの強制停止画面ですが、ボットは継続して動いているので、メモリリークなどが気になるところです。ということで、グラフかつ、前後の増減がスッと把握できるようにしました。(これは短期すぎて役に立たなさそうですが)

グラフの描画は雑に C3.js で行っており雑な JavaScript なので特にあらためて何もないのですが、グラフを描画する用のデータ配列に関しては、今後もちゃんと把握して真面目に実装していきたいので、package 化して再利用できるようにしました。

github.com

go heaper.Run(1, 60)

http.HandleFunc("/heaps", func(w http.ResponseWriter, req *http.Request) {
    heaps, _ := json.Marshal(heaper.Read())
    w.Header().Set("Content-Type", "application/json")
    w.Write(heaps)
})

こんな感じで pprof.WriteHeapProfile で得られる情報を任意の間隔 (1 sec)、任意の量 (60) ためておき、見れるという感じで、今後、どこを見ればいいのかわかるようになれば、それなりに役に立ってくれるはずです、多分。ただ単に画面が動いてるのが好きなのでやってみたという気がしないでもない。

Go 言語で struct を url.Values に展開した

golang 日記

現在勉強用に作っているボットでは、Slack の API を使います。勉強用なので、ライブラリを使わず、独自に実装しています。API ではエンドポイント毎にさまざまな要求パラメーターがあって、その引数を自由な形式にすると間違いのもとになるので、専用の struct を用意して間違いが起こらないようにしています。

func (a *API) ChatPostMessages(p ChatPostMessageParameters) (ChatPostMessagesResponse, error) {
    var r ChatPostMessagesResponse
    err := a.postAndStruct(&r, "/chat.postMessage", p)

    return r, err
}

public にしているメソッドはそのようなキッチリ定義した struct を引数として要求する一方で、色々なエンドポイントを叩くときのメソッド (postAndStruct など) は共通化したかったため、private にしてあるその共通メソッドの引数では、どんな struct でも受けつけて url.Values に変換、POST や GET したいと思いました。

というわけで、あまり推奨されていないであろう、reflect で struct をなんでも展開という方法を使います。

func (a *API) buildParameters(parameters interface{}) url.Values {
    values := url.Values{}
    r := reflect.ValueOf(parameters)
    rt := r.Type()

    for i := 0; i < rt.NumField(); i++ {
        f := rt.Field(i)

        key := ""
        if to := f.Tag.Get("json"); to != "" {
            key = to
        } else {
            key = strings.ToLower(f.Name)
        }

        v := fmt.Sprint(r.Field(i))

        if v != "" {
            values.Add(key, v)
        }
    }

    values.Add("token", a.Token)

    return values
}

interfrace{} として受け取った値を reflect.ValueOf(parameters) するのがキモで、それ以外は流れ作業でした。どうやれば interface{} -> struct できるかというのは github.com/fatih/structs というライブラリを読んで知りました。ソースコードがスッと読める言語は、こういう時に便利ですね。

github.com

Go 言語でつくったボットを GitHub -> CircleCI -> Bluemix と自動デプロイできるようにした

golang 日記

Golang でのボット作成では、1 個目は自 PC で動かすことしか考えていませんでしたが、2 個目はきちんとどこかにデプロイすることを目標に作成していましたので、ついでに自動でやれるようにしました。

Heroku では 24 時間稼働のボット用途には不向きだと思われたので、IBM Bluemix にデプロイ先に選びました。Bluemix もデフォルトで Golang をサポートしていますが、ビルトインの buildpack だと Godeps のバージョンをきちんと読んでくれなかったり (これは気のせいかもしれない) したのでgithub にあるものを利用したりして色々とあれでしたが、うまく行きました。

circle.yml の様子。

machine:
  environment:
    REPO_ROOT: "${HOME}/.go_workspace/src/YOUR_DOMAIN/YOUR_PACKAGE_DIR"

dependencies:
  pre:
    - mkdir -p ${REPO_ROOT}
    - cp -rf ./* ${REPO_ROOT}
    - go get github.com/tools/godep
    - curl -v -L -o cf-cli_amd64.deb 'https://cli.run.pivotal.io/stable?release=debian64&source=github'
    - sudo dpkg -i cf-cli_amd64.deb
    - cf -v

test:
  pre:
    - go vet ./...
  override:
    - cd ${REPO_ROOT} && godep go test ./...
  post:
    - cf api https://api.au-syd.bluemix.net
    - cf login -u $BLUEMIX_USER -p $BLUEMIX_PASSWORD
    - cf target -o $BLUEMIX_ORG -s $BLUEMIX_SPACE
    - cf a

deployment:
  production:
    branch: master
    commands:
      - cf push APP_NAME -b https://github.com/cloudfoundry/go-buildpack.git

とにかくプッシュ、テスト、デプロイまでをやれるようにとツギハギしたので、まだ無駄があると思いますが、とりあえずこれでアップできます。

つまずきポイントとしては、

  • ${HOME}/.go_workspace/src/ へリポジトリを展開しないと、go get -t -d -v ./... でリモートからパッケージ fetch しようとして死んだ (YOUR_DOMAIN を github ではないところにしているため、みつからない)
  • cf api でエリア指定を間違えた (だいたいの例はアメリカ南部になっているが、シドニーを使っている)
  • cf のインターフェースが gem install cf のものとちょっとちがう

などがありました。

基本的にはテストが通ったら cf で Bluemix にデプロイするというだけなので、すでに cf でのデプロイを済ませている人は、設定の際にあらためて考えることはない感じですね。

Slack で動いてるボットの処理が長い場合、フィードバックとしてインジケーターを出すということをやった

golang 日記

いま golang の練習用に作成しているボットには、URL をわたすと、そのサイトのキャプチャを撮影する機能があります。諸事情からボットのいるマシンとは別の場所、Heroku に設置していますが、起動が遅かったり、キャプチャ自体が遅かったりするので、ちゃんとやっていってるのかわかりません。そこで、ローディングインジケーターを出すことにしました。

できあがり

f:id:mmmpa:20161120045725g:plain

「こはる」とは社の Slack にいるボットですが、対抗して golang でつくっているのが「ごはる」です。かわいいですね。

アイコンの元ネタには以下を使わせてもらっております。

github.com

インジケーター

まずインジケーターを作成しました。実際はベタッと書いてから切りだしましたが、とにかくまずインジケーターを用意します。できあがりを想像してワクワクするのが原動力です。

type CaptureLoadingIndicator struct {
    indicators []string
    length     int
    step       int
}

func (c *CaptureLoadingIndicator) initialize(indicators []string) {
    c.indicators = indicators
    c.length = len(c.indicators)
    c.step = c.length - 1
}

func (c *CaptureLoadingIndicator) next() string {
    c.step = ((c.step + 1) % c.length)
    return c.indicators[c.step]
}

func createCaptureLoadingIndicator() *CaptureLoadingIndicator {
    c := (&CaptureLoadingIndicator{})
    c.initialize([]string{"▖", "▘", "▝", "▗"})

    return c
}

goroutine

キャプチャ機能では、キャプチャされた画像は Heroku からダイレクトに Slack に投稿されますが、キャプチャできたかどうかは Response で受けとれます。よって、Heroku へのリクエスト開始をインジケーターの開始、Response が帰ってくれば終了するとします。

細かい処理ははぶいて、goroutine 部です。http.PostForm は同期的に処理されますから、基本的に以降の処理をブロックします。これを goroutine に追いだして本プロセスとは並行に行いつつ、終わるまでの間はインジケーターを表示します。

Big Sky :: golang の channel を使ったテクニックあれこれを参考に、処理が終わるまで待つ処理を書きます。

// インジケーターを表示するメッセージの TS を取得する
var ind slack.ChatPostMessagesResponse
slack.ChatPostMessage.Strike(&ind, string(m.Channel), "starting...")

// 以下で goroutine ポーリングする
q := make(chan CaptureServerResult, 2)

go func() {
    // Heroku へリクエスト開始
    resp, err := http.PostForm(captureServer, values)
    // リクエスト終わり
    q <- CaptureServerResult{resp, err}
}()

indicator := createCaptureLoadingIndicator()
for {
    if len(q) > 0 {
        break
    }
    // 上記で取得した TS をターゲットとして、chat.update をかける
    slack.ChatUpdate.Strike(nil, string(m.Channel), ind.TS, fmt.Sprintf("loading `%v`", indicator.next()))
    // update 間隔は加減しましょう
    time.Sleep(200 * time.Millisecond)
}

slack.ChatUpdate.Strike(nil, string(m.Channel), ind.TS, "loaded")

resp := <- q
// response をもとにした処理が続く

<- q は処理をブロックしてしまうので、なんの動きもない重い処理、待ちの時に他の処理をさせておきたい場合はチェッカーとしては使えないんですね。

しかし、Channel をうまく利用することにより、簡単に実現することが出来ました (先達の知識に感謝)。こういうのは大体、開始や interval 処理ではなく、終了がめんどくさかったりするのですが、それが特に問題にならずよかったです。

あと func(){}() ができるのが色々便利ですね。

Ruby 外から Web アクセスする何か (Capybara とか、cli とか) を RSpec でテストするときのアクセス先をモックする

RSpec Ruby テスト

mmmpa.mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmpa.net

のテストをこの方法で行いました。

本編

たとえば、Capybara は各種ブラウザを介するアクセスのため、webmock が効かず、別の gem が必要なのは有名です。 https://github.com/oesmith/puffing-billy

Capybara テストする際はこれでいいのですが、Capybara テストする場合には不便かもしれませんね。

また、バッククォートや Open3 を用いて呼びだすような cli、たとえば siege などのターゲットをモックすることはできません。cli におんぶにだっこの gem を開発するときのテストに、かなり不便ですね。

RSpec 開始時にサーバーをたてる

望んだ結果を返すアクセス先があればいいので、テスト中に起動する仮想サーバーをたてます。

public ディレクトリに適当な HTML を用意した上で、

RSpec.configure do |config|
  require 'webrick'
  config.before(:suite) do
    port = ENV['MOCK_PORT'] || 3000
    host = ENV['MOCK_HOST'] || '127.0.0.1'
    started = false

    Thread.start do
      WEBrick::HTTPServer.new(
        DocumentRoot: File.expand_path('./public/', __dir__),
        BindAddress: host,
        Port: port,
        AccessLog: [],
        StartCallback: ->{ started = true }
        Logger: WEBrick::Log::new("/dev/null", 7)
      ).tap { |server|
        Signal.trap(:INT) { server.shutdown }
        server.start
      }
    end

    while !started
      sleep 0.5
    end
  end
end

Thread はあたらしいものを用意しないと、sever.start の時点で RSpec のプロセスが停滞してしまいますので注意しましょう。

また、別 Thread になるので server.start が完了するしないにかかわらずテストに突入、テストが落ちるということがあるので、StartCallback を使って、きちんとサーバーがスタートしたことを確認してからはじめましょう。

単純なアクセス以外

さらに、リダイレクトなど、単純な HTML アクセス以外の処理が必要ならば、インスタンス servermount すると処理を追加できます。

# たとえばリダイレクト
# Rails を模するなら、301(Moved) ではなく 302(Found)
server.mount_proc('/redirect') do |req, res|
  res.set_redirect(WEBrick::HTTPStatus::Found, '/redirected.html')
end

できあがり

これで、以下のようなテストが、自由に行えるようになりました。

expect(`siege -t 10s http://127.0.0.1:3000/foo.html`).to be_truthy

例は siege 自体のテストのようになってしまっていますが、実際は取得した結果をあれこれしたものをテストしました。

雑ではありますが、とても楽に、多くのアクセス先を用意できるようになりました。

siege をラップして多少細かい情報をまとめる gem 書いた。

Rails Ruby 日記

github.com

Usage

たとえば Rails 内でこうやる。

re = SiegeSiege.run(
      time: 20,
      concurrent: 4,
      user_agent: false,
      urls:
        [
          "http://localhost:3002#{students_path}",
          "http://localhost:3002#{students_path} POST name=abc",
          SiegeSiege::URL.new("http://localhost:3002#{students_path}", :post, {name: 'abc'}),
        ] + Student.ids.shuffle[0..2].map { |id| "http://localhost:3002#{student_path(id)}" }
    )

するとこういうのが得られる。

普通に使ってもコンソールに出てきてたやつ

re.total_result                                                                                                                                                                                                                                                                                                                                                                                  
=> {:command=>"siege -v -c 4 -t 20s -r 1 -R /tmp/20161030-21093-1lo07ca -f /tmp/20161030-21093-13f1kj6",
 :defaulting_to_time_based_testing=>{:value=>20.0, :unit=>"seconds"},
 :transactions=>{:value=>96.0, :unit=>"hits"},
 :availability=>{:value=>75.0, :unit=>"%"},
 :elapsed_time=>{:value=>19.47, :unit=>"secs"},
 :data_transferred=>{:value=>2.03, :unit=>"MB"},
 :response_time=>{:value=>0.8, :unit=>"secs"},
 :transaction_rate=>{:value=>4.93, :unit=>"trans/sec"},
 :throughput=>{:value=>0.1, :unit=>"MB/sec"},
 :concurrency=>{:value=>3.93, :unit=>""},
 :successful_transactions=>{:value=>96.0, :unit=>""},
 :failed_transactions=>{:value=>32.0, :unit=>""},
 :longest_transaction=>{:value=>10.65, :unit=>""},
 :shortest_transaction=>{:value=>0.01, :unit=>""}}

アクセスに使った URL 毎に平均レスポンス時間とか

re.average_log                                                                                                                                                                                                                                                                                                                                                                                   
=> [#<struct SiegeSiege::AverageLog id=6, url="http://localhost:3002/students/10763", count=17, secs=0.691, siege_url=#<SiegeSiege::URL:0x007fbee6efe870 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/10763">>,
 #<struct SiegeSiege::AverageLog id=0, url="http://localhost:3002/students", count=16, secs=1.276, siege_url=#<SiegeSiege::URL:0x007fbee6efeb18 @http_method=:get, @parameter={}, @url="http://localhost:3002/students">>,
 #<struct SiegeSiege::AverageLog id=5, url="http://localhost:3002/students/39284", count=16, secs=0.767, siege_url=#<SiegeSiege::URL:0x007fbee6efe938 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/39284">>,
 #<struct SiegeSiege::AverageLog id=7, url="http://localhost:3002/students/94576", count=17, secs=0.108, siege_url=#<SiegeSiege::URL:0x007fbee6efe7a8 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/94576">>,
 #<struct SiegeSiege::AverageLog id=3, url="http://localhost:3002/students", count=15, secs=1.367, siege_url=#<SiegeSiege::URL:0x007fbee6cd9518 @http_method=:post, @parameter={:name=>"abc"}, @url="http://localhost:3002/students">>,
 #<struct SiegeSiege::AverageLog id=1, url="http://localhost:3002/students", count=15, secs=0.651, siege_url=#<SiegeSiege::URL:0x007fbee6efea50 @http_method=:post, @parameter="name=abc", @url="http://localhost:3002/students">>]

標準出力をまとめたやつ

re.raw_log
=> [#<struct SiegeSiege::LineLog protocol="HTTP/1.1", status="200", secs=10.09, bytes=633, url="http://localhost:3002/students/10763", id=6, date=Sun, 30 Oct 2016 13:10:31 +0000, siege_url=#<SiegeSiege::URL:0x007fbee6efe870 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/10763">>,
 #<struct SiegeSiege::LineLog protocol="HTTP/1.1", status="200", secs=10.29, bytes=45579, url="http://localhost:3002/students", id=0, date=Sun, 30 Oct 2016 13:10:31 +0000, siege_url=#<SiegeSiege::URL:0x007fbee6efeb18 @http_method=:get, @parameter={}, @url="http://localhost:3002/students">>,
 #<struct SiegeSiege::LineLog protocol="HTTP/1.1", status="200", secs=10.24, bytes=633, url="http://localhost:3002/students/39284", id=5, date=Sun, 30 Oct 2016 13:10:31 +0000, siege_url=#<SiegeSiege::URL:0x007fbee6efe938 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/39284">>,
 #<struct SiegeSiege::LineLog protocol="HTTP/1.1", status="200", secs=0.25, bytes=633, url="http://localhost:3002/students/94576", id=7, date=Sun, 30 Oct 2016 13:10:41 +0000, siege_url=#
# 略

所感

カジュアルにテストできて便利ですね、みたいなツールで、詳細な情報を取ろうとするのはやめといた方がいいと思いました。(適切なやつをさがそう)

Capybara で Chromedriver をつかってモバイルモードでテストする時の Capybara.register_driver とか

RSpec Capybara 雑多なメモ

だいたいのサイトは User Agent で切り替えてると思うので、わざわざモバイルモードでテストが必要なのかしらとか思わないでもない (ダブルタップとか、スワイプをテストする?)、が一応メモ。

Capybara に Driver として登録

# chromedriver configuration

# chrome の起動オプションが使える http://peter.sh/experiments/chromium-command-line-switches/
# デフォルトではサブのディスプレイに表示してしまうので、ずらしている
default_args = %w(
  --window-position=2560,0
)

# ブラウザの外枠 (スクリーンショット撮って計測しよう)
chrome_frame_offset = {
  w: 10,
  h: 86
}

# プリセットの商品のセッティングを使う場合。
# DevTools で選べるモバイルから必要なものをピックアップ。(リストデータくれよ)
[
  {name: 'Apple iPhone 6 Plus', w: 414, h: 736}, # :apple_iphone_6_plus
  {name: 'Google Nexus 7', w: 600, h: 960},      # :google_nexus_7
].map { |configure|
  configure[:w] += chrome_frame_offset[:w]
  configure[:h] += chrome_frame_offset[:h]
  configure
}.each do |configure|
  Capybara.register_driver configure[:name].gsub(' ', '_').downcase.to_sym do |app|
    Capybara::Selenium::Driver.new(
      app,
      browser: :chrome,
      desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome(
        'chromeOptions' => {
          args: [
            "--window-size=#{configure[:w]},#{configure[:h]}"
          ] + default_args,
          mobileEmulation: {deviceName: configure[:name]}
        }
      )
    )
  end
end

# 独自のセッティングでモバイルモードを使う場合
# これはイッパツで全画面をスクリーンショットしたかった時の設定
Capybara.register_driver :chrome_dummy_mobile do |app|
  w = 320
  h = 1600

  Capybara::Selenium::Driver.new(
    app,
    browser: :chrome,
    desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome(
      'chromeOptions' => {
        args: [
          "--window-size=#{w + chrome_frame_offset[:w]},#{h + chrome_frame_offset[:h]}"
        ] + default_args,
        mobileEmulation: {
          deviceMetrics: {
            width: w,
            height: h,
            pixelRatio: 2.0
          },
          userAgent:
            'Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_2 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13F69 Safari/601.1'
        }
      }
    )
  )
end

選んで使う

# 登録しておいた driver から選ぶ
Capybara.configure do |config|
  config.run_server = false
  config.default_driver = :apple_iphone_6_plus
  config.app_host = 'http://googole.com/'
end