SendGridで1つのメールを最大1000件のメールアドレスにまとめて送信する方法
これはよく見るとドキュメントに載っているのだけど、 結構見落としがちだったので、記載しておく。
ドキュメントの内容
v3 Mail Send API概要 - ドキュメント | SendGrid
SendGridのMailSendAPIを使えばOKな感じです。
Rubyのコード
これで終了だとあまりにもつまらないので、 SendGrid公式でRubyのGem(SDK)が提供されているので、それを使った際のサンプルを載せておく。 Rubyのコードにするとざっくりこんな感じだろう。
def mail_send sendgrid = SendGrid::API.new(api_key: ENV['SENDGRID_API_KEY']) sendgrid.client.mail._("send").post(request_body: params) end def params param = { "custom_args" => { "test_mode" => true }, "from" => { "email" => 'example@example.com', "name" => 'サイト名' }, "content" => [ { "type" => 'text/html', "value" => 'テスト' } ], "mail_settings" => { "sandbox_mode" => { "enable" => false } } } param["personalizations"] = User.all.find_each do |user| { "to" => [ { "email" => user.email, "name" => user.name } ], "substitutions" => { "[USER_NAME]" => user.name, "[USER_EMAIL]" => user.email, }, "subject" => 'メールタイトル', "custom_args" => { "site_user_id" => user.id.to_s, } } end param end mail_send
この方法で運用すれば1000つの宛先へのメール送信をまとめることができる。 配信メールアドレスが30000件なら、30回のリクエストに分割してメール配信リクエストをSidekiqやDelayedJob等でWorkerの数を調整して処理してやれば送信リクエストはサクッと完了するだろう。 ただ、流石に30,000件とかのユーザーへの一斉送信はMarketing Campaigns API Overview - ドキュメント | SendGrid を使うべきでは…? と思ってしまっています。*1
テンプレートに変数をアサイン
他にもMailAPIでは、テンプレートを変数を定義しておき、送信時に変数を展開することができる。(コードのsubstitutions
あたりを参照してください。)
それぞれのユーザーでメールアドレスだけでなく、ユーザー名や宛名なんかも異なるだろうし、メールの文章中にそういった動的な文言も含めることができる。
まさにSendGridさまさまである。
ransackで雑にRDBMSを使った簡易的なキーワードのOR検索を実装する
一般的なRDBMSを使ったよくあるLIKEでの曖昧検索実装では、検索ボックスに「寿司 和食」と入力してもただの単語での曖昧検索だと、
当たり前なんだけど、キーワードのOR検索を実現することができない。
select * from contents where LIKE body '%寿司 和食%'
となってしまって、空白を含めた1単語として認識されてしまい、思うような結果が得られないと思う。
実際に組み立てるクエリは…?
多分イマイチなんだろうけど、簡易的にOR検索を実現するにはこんな感じになると思う。
"SELECT "contents".* FROM "contents" WHERE (("title" LIKE '%寿司%' OR "title" LIKE '%和食%') OR ("body" LIKE '%寿司%' OR "body" LIKE '%和食%'))"
これで擬似的にキーワードのOR検索を実装することができた。精度もまぁ無いよりかはマシくらいなもんだろう。 これでOKな事例というのは、検索対象が小規模な場合で、設定できるキーワードに上限を一応設けたほうが良いかもしれない。 以上の注意点を踏まえた上で、Railsで簡単に上記SQLを作る方法に進んでみよう。
実際にRansackで雑に実装
もちろん複雑なことはやっていないので、自分でさっきのSQL文を組み立てて投げるのでも良いのだけど、 面倒だし、検索機能を実装する時によく使われているRansackで実装してみようと思う。
以下のようにかけばOK。
Content.search(title_or_body_cont_any: ['寿司', '和食']).result
それでは上記で実行されるSQLを見てみよう。
irb(main):007:0> Content.search(title_or_body_cont_any: ['寿司', '和食']).result.to_sql => "SELECT \"contents\".* FROM \"contents\" WHERE ((\"contents\".\"title\" LIKE '%寿司%' OR \"contents\".\"title\" LIKE '%和食%') OR (\"contents\".\"body\" LIKE '%寿司%' OR \"contents\".\"body\" LIKE '%和食%'))"
おお、大丈夫そうだ。 あとは、JOINでスペースを区切り文字にして、splitでキーワードの配列作ればOKな感じ。
# controller def index if params[:keyword] @contents = Content.search(title_or_body_cont_any: params[:keyword].split(' ')).result else @contents = Content.all end end
これで完成。 意外とシュッとできた。やっぱり小規模用途だとransackは便利だった。
SendGridのWebhookを受けるのにFluentdが重宝した話
SendGridのWebhookについて
SendGrid の Event Webhook は、SendGrid経由でメールを送信する際に発生するイベントを、指定したURLにPOSTすることができます。 このデータの用途は、配信停止アドレスの削除、迷惑メール報告への対応、エンゲージできなかった受信アドレスの判定、バウンスされたメールアドレスの特定、メールプログラムの高度な分析などです。ユニーク引数やカテゴリパラメータを使用して、動的なデータを挿入することができるため、あなたのメールのシャープでクリアなイメージを構築するのに役立ちます。
ということなんだけど、 1度に送信件数が5000件以上だったりするとそれなりにOpenや送信したというイベントが飛んで来るので、 このWebhookをどうさばくかというのがちょっとした課題になる。 多分BigQueryに送信するだけであればGCPのLambda相当のサービスを利用すればOKな気がする。 というかGCPがサンプル乗っけてるので、コレはコレで便利なのでぜひ見て欲しい。
SendGrid Tutorial | Cloud Functions Documentation | Google Cloud
Webhookを受けて、レスポンス内容をMongoに格納する例
今回はFluentdのデフォルトに同梱されているHTTP Input Pluginを利用して、Webhookを受けて、Mongoに送信する方式を取った。 Webhookを受けるだけのなにかアプリケーションを作る必要あったのかと思ったけど、Fluentdでシンプルに解決できる方法があってよかった。
Fluentdのプラグインとして追加が必要なのは、fluent-plugin-mongo
くらい。
そのためのfluentd.confをココにおいておく。
<source> @type forward </source> <source> @type http port 8080 </source> <filter **> @type stdout </filter> <match sendgrid.**> @type mongo database "#{ENV['DB_NAME']}" host "#{ENV['DB_HOST']}" port "#{ENV['DB_PORT']}" collection "#{ENV['DB_COLLECTION']}" user "#{ENV['DB_USER']}" password "#{ENV['DB_PASSWORD']}" capped capped_size 100m ssl true </match>
これでFluentdを再起動し、SendGrid上でport:8080を接続先に設定すればOK。
そもそもBigQueryやTresureDataではなく、 Mongoに送っている理由としては、短い保持期間であればMongoの方が短い時間で結果が返ってくるのかなということでそうしている。 この場合たくさんデータがあふれてくると結局処理時間が長くなるのでは??? という懸念があるので、格納するデータの保持期間については別途検討する必要がありそう。
保持期間を設定してそれまでは保存しておき、古いドキュメントを自動で削除するみたいなのが、 少し簡単にできるかも。今後はコレを参考にして検証する予定である。 qiita.com
サンプルはMongoに送信しているだけだけど、TresureDataやBigQuery、はたまたフォーマッティングして、 S3にもOutputするようにすれば良いんじゃないですかねという感じ。
余談だけど、どこでも動かせるようにということでDockerイメージに固めている。 Azureを利用しているのでWeb App for Containersで動かしている感じ。 多分AWSではECSでも良いだろうし、きっともっと面倒ならElasticBeanstalkでも良いと思う。
Webhookを受けるのにも便利なFluentd
Fluentdはバッファリングしてくれる機構を持っているし、こういったものを自前で実装するよりも信頼性は高い。 Webhook受けて何処かに書き込むような簡単な処理だけなら、わざわざRailsアプリケーションを立てる必要はなくFluentdで十分だと思っている。*1
*1:ロスト確率と、対応時間のコストの兼ね合い考えると、少し重複する分に問題ないなら良い選択と思っている。
Firebase HostingをCircleCIからDeployする
とにかく簡単なので、プロトタイピングのときから初めておくといい。
fierebaseを使ったことがあるなら入っていると思うけど、firebase-tools
を事前にインストールしておこう
npm install -g firebase-tools
CIからDeployするためのDeployキーを取得する。
firebase login:ci
って打つとCI用のDeployキーを払い出してくれる。
Waiting for authentication... ✔ Success! Use this token to login on a CI server: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Example: firebase deploy --token "$FIREBASE_TOKEN"
払い出されたキーはCircleCIの環境変数に登録して、Deploy時に実行すればOKという感じ。 ほんとうに簡単。
ちなみにプロジェクトを指定してDeployしたい場合は、このように指定すればオッケー
firebase deploy --project "myproject-xxxxx" --token "$FIREBASE_TOKEN"
FaradayでHTTPリクエストを並列で実行する方法
FaradayはRubyでHTTPリクエストを行うことができるライブラリだけど、 特に工夫もなしに利用すると1つ1つリクエストを順番に直列に実行するようになっている。
もしなにかしらの事情でAPIを並列に叩きたい事情というものもあるので、 その場合は以下の用に対応すればOK。 事前にtyphoeusというGemが必要なので、予めインストールしておこう。
require 'typhoeus' require 'typhoeus/adapters/faraday' response1, response2 = nil conn = Faraday.new(:url => "http://example.com") do |faraday| faraday.adapter :typhoeus end conn.in_parallel do response1 = conn.get('/one') response2 = conn.get('/two') # these will return nil here since the # requests haven't been completed response1.body response2.body end # at this point the response information you expected should be fully available to you. response1.body # response1.status, etc response2.body
こちらにはもう少し詳しく記載されている。 github.com
さらに、Faraday別に使わなくてもtyphoeus単体で利用することも当然可能です。 Faraday別にいらなくね?という場合はこちらを。
で、効果の程は...?
一体どのくらい早くなるのかと、処理した結果の順番が保証されているかが気になるところなので、 雑に実際のAPIにリクエストして簡単に比較してみることにした。
業務で20個のAPIに対してRubyでHTTPリクエストをする必要が出たので、簡単にAPIを20個叩くようなサンプルを作って雑にベンチ取った。 実際のエンドポイントを叩いているのだけど、これは自分の持っているWebhook送信のデバッグ用のアプリケーションなので、お気になさらずに。
gist22ea9304be7d995147528f0e6713ba59
まとめ
- 20個のHTTPリクエストは並列化したほうが早い
- 並列処理の場合、処理結果の順番とかに変更がないか気になるところだけど、順番は保証されているようだった。