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

More than 1 year has passed since last update.

Android の世界には FCM (Firebase Cloud Messaing) という、Android 端末に push notification を送るためのプロトコルがある。そして、Ruby の世界には fcm gem という、そのプロトコルを簡単に叩くためのライブラリが存在する。

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


  • 必要もなく httparty gem と json gem に依存している。


  • Persistent connections を使っていないので、とてつもなく遅い。

  • 単にコードの質が低い。

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


The andpush gem

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

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 と大差ないが、ベンチマークを取ってみるとその差は歴然:

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 で提供される persistent connections を使っているので、一番最初のリクエストだけで SSL のハンドシェイク等が行われ、それ以降は実際のリクエストだけがやりとりされるので速い。たぶん。実は HTTP にそこまで詳しくないので詳しい人教えて下さい。


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

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


結論


  • 遅い実装には遅い理由があり、速い実装には速い理由がある。

  • Pipelining を使えばもっと速くなるだろうが、レスポンスや例外の処理が悩ましくな流のでそれはまた今度。

  • コードを読まずに名前やスターの数だけで判断して gem を使うのはやめよう。

  • fcm gem よりも andpush gem を使おう。

  • andpush gem のフィードバックお待ちしています。