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
endlib/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ではなく、単なる非同期送信のようでした。
以上になります。
少しでも参考になれば幸いです。
