PrAha Inc.のCEO兼エンジニアのdowannaです
サービス内でなんらかのアクションがあった時にユーザにメール通知したい時、ありますよね。
でもサーバにメール送信まで任せてしまうと、どこまでサーバに対して負荷がかかるか心配です。
そんな時はPubSubなどのキューイングサービスを使って、メール送信機能を本体から切り離したいもの。
今回の記事はPubSubとSendGridを使ったメール送信機能のマイクロサービスを作ってみます。
やりたい事
- 送信予定のメールをPubSubのmessageとしてキューしておく
- 1日1回、PubSubからmessageをpullしてメール送信
- メール送信が完了したらmessageを削除
大まかな作業の流れ
- Topic, Subscriptionを作成
- Topicに向けてmessageを配信
- SubscriberClientを作成して、messageを受信
- messageを使ってメールをsendgridから送信
- 送信を確認次第、messageをacknowledgeしてPubsubから削除
Topicに向けてmessageを配信
まずはPubSubにTopicを作成する必要がある。
Topic作成方法1:GCPコンソール
コンソールを開き、Pub/Subを選択して、画面上の「トピックを作成」から作成する
Topic作成方法2:CLIから以下コマンドを実行
ローカルで認証済みのCLI、もしくはGCPコンソールから「クラウドシェル」を起動して、Topic作成
gcloud pubsub topics create my-topic
SDK使ってTopicを作る事も可能だけど、ひとまずこの2つを知っておけば困らないのではなかろうか。
Topicにmessageを飛ばす
import { PubSub } from '@google-cloud/pubsub'
const pubsub = new PubSub({ projectId: 'some-project-id' })
const topic = await pubsub.topic('my-topic')
await topic.publishJSON({ text: 'hello!' })
これだけで、先ほど作成した my-topic にメッセージが飛ぶ。
例えばユーザが誰かに「いいね」を送った時にメール通知したいのであれば、いいね直後にサーバからtopicにpublishすれば良い。
メール送信の負荷とか、送信の成否とか一切気にする事無く、ただPubSubに丸投げすれば良いので、
システムの依存性や関心の範囲を少し下げられる。
今回はメール送信に使う本文 'hello!' をPubSubに貯めてみた。
1日1回、PubSubからmessageをpullしてメール送信
まずはコンソールでmessageをpullしてみる
まずメッセージを取得するためには、Topicだけではダメ。
Subscriptionも作成しなければいけない。
CLIで、こんなコマンドを叩いて
topic: my-topic に
subscription: my-sub を追加する
gcloud pubsub subscriptions create --topic my-topic my-sub
これでmessageをpull出来るようになった。
試しにさっきpublishJSONしたmessageを確認してみよう。
コンソール画面から、my-topicを選択して「メッセージを表示」をクリックする
先ほど作成したsubscriptionを選択する
そしてPULLをクリックする
先ほど送信したmessageが取得できる事が確認した。
あとは今のコンソールと同じことを、コードからクライアントライブラリを使って実装してみよう。
クライアントライブラリを使ってmessageをpullしてみる
pullの方法は大きく分けて非同期/同期pullが用意されている。
今回は「すでに登録されているmessageをまとめて取得したい」ので、
公式サイトに載ってるsynchronous pullを使ってみる。
まずはv1パッケージをimportしてSubscriberClientを初期化する
import { v1 } from '@google-cloud/pubsub'
const subClient = new v1.SubscriberClient()
続いてclientに「どのSubscriptionからmessageを取得するか、および取得上限数」を定義する。
const formattedSubscription = subClient.subscriptionPath(
projectId, // 今回なら some-project-id
subscriptionName // 今回なら my-sub
);
const request = {
subscription: formattedSubscription,
maxMessages: 10, // とりあえず上限10通
};
最後にpullする。これでコンソールで実行したのと同じ事が実装できた。
const [response] = await subClient.pull(request);
ちなみにこの時点では、まだmessageは削除していない。
メール送信が完了したらmessageを削除
ここから先は各々の実装により異なるけど、例えばsendGridを使っている場合を想定すると、こんな感じ。
const ackIds = []
for (const receivedMessage of response.receivedMessages) {
SendGrid.send({
to: 'hogehoge@hogemail.com',
from: 'fugafuga@hogemail.com',
subject: 'hello world',
text: receivedMessage.message.data.toString() // ByteなのでStringに変換しておく
})
// 失敗した場合の処理はここでは考えていない
ackIds.push(receivedMessage.ackId) // あとで削除するためにAcknowledgeIdを保管しておく
}
実務であれば「メール送信に失敗した場合、どうする?message消しちゃって大丈夫?」と検討するけれど、ここでは一旦気にせずackIdsにpushしちゃおう。
全てのメール送信タスクを投げるのに完了したら、これでmessageを一括消去する
const ackRequest = {
subscription: formattedSubscription,
ackIds: ackIds
}
await subClient.acknowledge(ackRequest)
acknowledgeされたmessageはPubSubから消える。
以上でPubSubを使ったメール配信の実装が完了した。
まとめ
- Topic, Subscriptionを作成
- Topicに向けてmessageを配信
- SubscriberClientを作成して、messageを受信
- messageを使ってメールを送信
- 送信を確認次第、messageをacknowledgeしてPubsubから削除