Cloud PubSubとSendGridを使ってメール送信機能を切り離す

PrAha Inc.のCEO兼エンジニアの[dowanna](https://twitter.com/dowanna6)です

サービス内でなんらかのアクションがあった時にユーザにメール通知したい時、ありますよね。
でもサーバにメール送信まで任せてしまうと、どこまでサーバに対して負荷がかかるか心配です。

そんな時は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作成方法１：GCPコンソール
コンソールを開き、Pub/Subを選択して、画面上の「トピックを作成」から作成する

#### Topic作成方法２：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を選択して「メッセージを表示」をクリックする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246648/fd109f96-0824-49e8-4e75-e0efc7ac7876.png)

先ほど作成したsubscriptionを選択する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246648/f20a7b85-32e2-ff5d-8240-a87234909469.png)

そしてPULLをクリックする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246648/8766a049-8176-9df9-ed9d-e2ea8eb4e152.png)

先ほど送信したmessageが取得できる事が確認した。
あとは今のコンソールと同じことを、コードからクライアントライブラリを使って実装してみよう。


#### クライアントライブラリを使ってmessageをpullしてみる

pullの方法は大きく分けて非同期/同期pullが用意されている。
今回は「すでに登録されているmessageをまとめて取得したい」ので、
[公式サイト](https://cloud.google.com/pubsub/docs/pull#synchronous_pull)に載ってる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から削除