fcmpushを利用してHTTP/2で多重送信するための実装を紹介します。
既にfcmpushを利用していて、FCMの/batchエンドポイントが使えなくなり、困っている方向けの内容になっているため、fcmpushの使い方については割愛しています。

事件

社内から「push通知が届かない」という報告を受け、ログを確認すると、9月上旬あたりからちらほらFCMのレスポンスで「Operation is not implemented, or supported, or enabled.」というエラーが出始めていて、報告を受けた数日前からは完全に全件エラーになっていることが発覚しました。

慌てて調査した結果、push通知送信に使っていたFCMの/batchエンドポイントがとっくに廃止になっているようでした😱
https://firebase.google.com/support/faq?hl=en#fcm-depr-features
※ちなみに日本語のマニュアルには記載がなく、24/10/11現在でも/batchを利用するよう書かれていたりします。

とにかく時間がない!どうやって対応しよう!

リリース済みのアプリにも影響が出ているため、とにかく時間がありません。
push通知送信には「fcmpush」というgemを利用していたため、今回はこれをそのまま利用しつつ、FCMへの送信部分だけモンキーパッチで上書きすることにします。

対応

Gemfileでgem追加

gem 'http-2'

モンキーパッチ作成

config/initializers/monkey_patches.rb

Dir[Rails.root.join('lib/monkey_patches/**/*.rb')].sort.each do |file|
  require file
end

lib/monkey_patches/fcmpush/client.rb

「プロジェクトID」はご自身のものに差し替えてください。
また、私はFCMからの戻り値をログに出力しているのでレスポンスを返すようにしていますが、必要なければ「stream.on」のコールバックなど、responses変数周りの処理は省略しても問題ありません。

require 'http/2'
require 'socket'
require 'openssl'

module FcmpushClientMonkeyPatch
  HTTP2_BATCH_ENDPOINT = "/v1/projects/プロジェクトID/messages:send".freeze
  REQUEST_TIMEOUT_SEC = 60.freeze # タイムアウト(秒)

  def batch_push(messages, query: {}, headers: {})
    uri = URI.parse(domain)
    host = uri.host

    # SSL接続の準備
    tcp = TCPSocket.new(host, 443)
    ctx = OpenSSL::SSL::SSLContext.new
    ctx.alpn_protocols = ['h2']  # HTTP/2プロトコルを指定
    # ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
    ssl = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
    ssl.sync_close = true # SSLソケットが閉じられたらTCPも閉じる
    ssl.connect

    # HTTP/2クライアントの作成
    client = HTTP2::Client.new

    # SSLソケットをラップする
    client.on(:frame) do |bytes|
      ssl.write(bytes)
    end

    access_token_refresh
    headers = v1_authorized_header(headers)

    responses = []

    # 複数のストリームを作成してリクエストを送信
    messages.each do |message|
      stream = client.new_stream

      headers = headers.merge({
        ':scheme' => 'https',
        ':method' => 'POST',
        ':authority' => host,
        ':path' => HTTP2_BATCH_ENDPOINT,
        'content-type' => 'application/json'
      })

      response_body = ''

      stream.on(:data) do |data|
        response_body << data
      end

      stream.on(:close) do
        responses << response_body
      end

      # ストリームにヘッダーとボディを送信
      stream.headers(headers, end_stream: false)
      stream.data(message.to_json, end_stream: true)
    end

    begin
      Timeout.timeout(REQUEST_TIMEOUT_SEC) do
        # サーバーからのデータを非同期に受信
        while client.active_stream_count > 0
          data = ssl.read_nonblock(1024, exception: false)
          client << data unless data == :wait_readable
        end
      end
    rescue Timeout::Error
      # この時点でストリームに送信されているリクエストがあれば実行はされるが、接続を閉じるためログには残らない
      responses << JSON.generate({ error: 'timeout' })
    ensure
      ssl.close
    end

    responses
  end

  private
end

Fcmpush::Client.prepend(FcmpushClientMonkeyPatch)

エラーハンドリングなどはよしなに追加してください。
タイムアウト処理のところでタイミングによってはログに記録できないものが出てしまうのが気になりますが…とりあえず今は目をつむります。

受け取り側

変更はありません。
以下のようになっていればそのまま使えます。

client = Fcmpush.new(プロジェクトID)
responses = client.batch_push(FCMに送信するペイロードのhash)

とりあえずこれで実機でpush通知の受信が確認できました。

送信ストリーム数に上限があるので注意

HTTP2::Error::StreamLimitExceeded

これまでは500件を上限として分割送信していましたが、今回の送信方法ではエラーになりました。
とりあえず100件まで下げたところ、無事に送信できたのでご注意ください。

本当に多重送信できているのか確認したかったが…

サーバーのログなどで本当に多重送信になっているのか(実際は単純に複数リクエストを同時送信しているだけになってたりしないか)などを確認したかったのですが、確認用のサーバーを用意する時間もないため、今回はそこまで確認できませんでした。

今回は見送ったgem達

最終的な形になるまで、fcmpushから簡単に乗り換えられるものはないか、簡単に多重送信できるものはないか、と色々探して試したりしていました。
なぜ見送ったのか理由も書いておきますので、gem選定の参考にしてください。

andpush

https://github.com/yuki24/andpush

fcmpushからの乗り換えでFCMへの送信からHTTP/2の多重送信までカバーできそうに思いましたが、単体での送信しかサポートしてないようで、結局多重送信部分は自前で実装する必要がありそうでした。
あと、最終リリースが2018年なのでメンテされてない様子が不安でした。

net-http2

https://github.com/ostinelli/net-http2

多重送信周りで利用できるか検討していましたが、httpsで通信するとエラーになるというissueと同じ現象にて諦めました。

typhoeus

https://github.com/typhoeus/typhoeus

多重送信周りで利用できるか検討していましたが、複数リクエストを同時送信してくれてはいるものの、Multiplexingではなく、単なる非同期送信のようでした。

以上になります。
少しでも参考になれば幸いです。



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる