andpush という FCM（GCM）のクライアント gem を書いた

Android の世界には [FCM (Firebase Cloud Messaing)](https://www.google.com/search?q=fcm+google&oq=fcm+google&aqs=chrome..69i57j69i60j0l4.1535j0j7&sourceid=chrome&ie=UTF-8) という、Android 端末に push notification を送るためのプロトコルがある。そして、Ruby の世界には [fcm gem](https://github.com/spacialdb/fcm) という、そのプロトコルを簡単に叩くためのライブラリが存在する。

この fcm という、まるで Google 公式 gem であるかのような名前なのだが、全然公式ではないし、コードを読んでみるとこれがかなりやばい。具体的に何がやばいかというと、

 * 必要もなく [httparty gem](https://github.com/jnunemaker/httparty) と json gem に依存している。
 * [Persistent connections](https://ja.wikipedia.org/wiki/HTTP%E3%81%AE%E6%8C%81%E7%B6%9A%E7%9A%84%E6%8E%A5%E7%B6%9A) を使っていないので、とてつもなく遅い。
 * 単にコードの質が低い。

このレベルだとpull request を送って改善するのも無理そうだし（json gem を依存関係から削除するというのは [PR 送った](https://github.com/spacialdb/fcm/pull/26)けど）、新しいものを作った方が世界が幸せになれそうだと思ったので、andpush という gem を作った。

# [The andpush gem](https://github.com/yuki24/andpush)

例によって `gem 'andpush'` するか、`gem i andpush` すればすぐに使える。

```ruby
require 'andpush'

server_key   = "..." # Your server key
device_token = "..." # The device token of the device you'd like to push a message to

client  = Andpush.new(server_key)
payload = {
  to: device_token,
  notification: {
    title: "Update",
    body: "Your weekly summary is ready"
  },
  data: {
    extra: "data"
  }
}

response = client.push(payload)

json = response.json
json[:canonical_ids] # => 0
json[:failure]       # => 0
json[:multicast_id]  # => 8478364278516813477
```

と、使い方は fcm gem と大差ないが、ベンチマークを取ってみるとその差は歴然:

```ruby
require 'benchmark/ips'
require 'andpush'
require 'fcm'

server_key   = ENV.fetch('FCM_TEST_SERVER_KEY')
DEVICE_TOKEN = ENV.fetch('FCM_TEST_REGISTRATION_TOKEN')

FCM_CLIENT      = FCM.new(server_key)
PAYLOAD_FOR_FCM = { dry_run: true, notification: { title: "u", body: "y" } }

ANDPUSH_CLIENT      = Andpush.build(server_key)
PAYLOAD_FOR_ANDPUSH = PAYLOAD_FOR_FCM.merge(to: DEVICE_TOKEN)

Benchmark.ips do |x|
  x.report("andpush") { ANDPUSH_CLIENT.push(PAYLOAD_FOR_ANDPUSH) }
  x.report("fcm")     { FCM_CLIENT.send(DEVICE_TOKEN, PAYLOAD_FOR_FCM) }

  x.compare!
end
```

結果:

```
$ ruby bench.rb
Warming up --------------------------------------
             andpush     2.000  i/100ms
                 fcm     1.000  i/100ms
Calculating -------------------------------------
             andpush     27.903  (± 7.2%) i/s -    140.000  in   5.063236s
                 fcm      7.232  (±13.8%) i/s -     35.000  in   5.020404s

Comparison:
             andpush:       27.9 i/s
                 fcm:        7.2 i/s - 3.86x  slower
```
_（benchmark のために dry_run: true にしているが、Google さんごめんなさい。）_

ご覧の通り、4倍近い throughput  が出た。これは、短時間に大量の push notification を送りたい場合に非常に重要である。

## なぜ速いの？

[net-http-persistent gem](https://github.com/drbrain/net-http-persistent) で提供される persistent connections を使っているので、一番最初のリクエストだけで SSL のハンドシェイク等が行われ、それ以降は実際のリクエストだけがやりとりされるので速い。たぶん。実は HTTP にそこまで詳しくないので詳しい人教えて下さい。

## net-http-persistent gem だって依存の一つではないか？

その通りであるが、作者の [Eric Hodel](https://twitter.com/drbrain) は Ruby committer だし、個人的には Net::HTTP と同じような感覚で使っている。中身もざっくり読んで問題無さそうなので、自分で書くよりはこれを使う方がいいだろうと判断した。さらに、net-http-persistent は [connection_pool gem](https://github.com/mperham/connection_pool) というまた別の gem に依存しているが、これは [sidekiq](https://github.com/mperham/sidekiq) の作者 [Mike Perham](https://twitter.com/mperham) によって作られたものなので、（そこまでじっくりコードは読んでないものの、ある程度）信頼できる。これ以外の依存は無い。

# 結論

 * 遅い実装には遅い理由があり、速い実装には速い理由がある。
 * Pipelining を使えばもっと速くなるだろうが、レスポンスや例外の処理が悩ましくな流のでそれはまた今度。
 * コードを読まずに名前やスターの数だけで判断して gem を使うのはやめよう。
 * fcm gem よりも andpush gem を使おう。
 * andpush gem のフィードバックお待ちしています。