🌃

【Rails】深夜にメールを送らない deliver メソッドを ActionMailer に追加する方法

2024/10/03に公開

ラブグラフでエンジニアをしております横江( @yokoe24 )です!

Rails の ActionMailer には deliver_nowdeliver_later というメソッドがあります。

メール送信に関して deliver_now は即時実行、
deliver_laterActiveJob で sidekiq などを介し、非同期で実行をおこないます。

深夜のメール送信を避けたいという要望

ラブグラフでは、ラブグラファー(カメラマン)さんによって写真が納品されたタイミングで
ゲストさんに納品完了のメール送信をおこなうのですが、
納品のタイミングによっては深夜にメールが届いてしまうことが起こり得ます。

遅い時間にメールが来て通知音が鳴り響いて起きる、などあるとゲストさんに迷惑がかかるので、
深夜にはメール送信がされないよう、 deliver_later の引数として、送信時間が指定されていました。

current_time = Time.zone.now

if current_time.hour > 22
  send_time = (current_time + 1.day).change(hour: 8)
  ClientMailer.test_email.deliver_later(wait_until: send_time)
elsif current_time.hour < 8
  send_time = current_time.change(hour: 8)
  ClientMailer.test_email.deliver_later(wait_until: send_time)
else
  ClientMailer.test_email.deliver_later
end

たしかこんな感じの処理が書かれていました。なかなかに長いです。

共通化しよう!

深夜のメール送信を避けたい箇所は複数あります。
そこに毎回長い処理を書くのは冗長ですから、共通化したいですね!

今回は deliver_nowdeliver_later のように、
deliver_avoid_midnight というメソッドを作ることにしました。

deliver_now などのメソッドは
ActionMailer::ActionMailer 内に定義されています。

https://github.com/rails/rails/blob/v7.1.3.4/actionmailer/lib/action_mailer/message_delivery.rb#L123-L129

Rails の initializers でメソッドを定義することにします。

# config/initializers/deliver_avoid_midnight.rb
class ActionMailer::MessageDelivery < Delegator
  HOUR_NOT_SEND_EMAIL_FROM = 22 # この時間(xx:00)以降にはメール送信をしない
  HOUR_NOT_SEND_EMAIL_TO = 8 # この時間(xx:00)より前にはメール送信をしない

  def deliver_avoid_midnight
    current_time = Time.zone.now
    current_hour = current_time.hour

    if HOUR_NOT_SEND_EMAIL_FROM <= current_hour
      send_time = (current_time + 1.day).change(
        hour: HOUR_NOT_SEND_EMAIL_TO,
        min: current_time.min,
        sec: current_time.sec,
      )

      deliver_later(wait_until: send_time)
    elsif current_hour < HOUR_NOT_SEND_EMAIL_TO
      send_time = current_time.change(
        hour: HOUR_NOT_SEND_EMAIL_TO,
        min: current_time.min,
        sec: current_time.sec,
      )

      deliver_later(wait_until: send_time)
    else
      deliver_later
    end
  end
end

これで、今まで deliver_later と書いていたところを
deliver_avoid_midnight と書き直すだけで、
深夜のメール送信を防ぐことが出来るようになりました!

この実装のデメリット

今回のような深夜にメールを送らないようにする実装ですが、憂慮すべき点がいくつかあります。

1: キューが溜まってしまう

メールを送らないぶん、sidekiq にキューが溜まってしまいます。
sidekiq のキューを管理するDBとして Redis などインメモリデータベースを使用している場合は、
メモリ消費量が限界を迎えてしまう可能性がありえます。

限界を迎えると何が起きるかと言うと、 Redis のデータが飛びます!
過去のキューが動かなくなってしまうので、これが起きると大変です。

参考: Redisのメモリが100%を超えたときの挙動の調査・検証 #AWS - Qiita

2: メールが前後して届く可能性

1:43 と 2:11 にメールの送信処理が deliver_avoid_midnight メソッドに渡された場合、
それぞれ 8:43, 8:11 のメール送信に変更されますので、
本来の順番と前後してしまいます。

順番が前後すると違和感のあるメール同士が存在する場合には、
今回のような処理をおこなわないといいでしょう。

次に記載する 3. の問題が起こりやすくなりますが、
溜めたメールすべてを 8:00 にまとめて送信するのなら、メールが前後する問題の影響を受けにくくなります。

3: AWS SES の送信数制限にひっかかる可能性

これはあまり無い可能性ですが、
メールを送る時間が8時台に固まることで、
サービスによっては AWS SES の送信クォータに引っかかってしまう可能性があります。

通常、運用しているサービスで1秒あたりに送信できる件数は増やしていることかと思いますし、
失敗したとしても、今回のように sidekiq での実行なら再実行されますので問題ないかとは思いますが、
この点も気にしておくといいでしょう。

なお、今回例に挙げた元の実装では

send_time = (current_time + 1.day).change(hour: 8)

となっていて、この change メソッドが hour だけを変更してくれるように見えるのですが、
実は change メソッドは min と sec を自動で 0 に設定するという挙動があります。

https://github.com/rails/rails/blob/v7.1.3.4/activesupport/lib/active_support/core_ext/time/calculations.rb#L142-L144

ですから、元の実装だとすべてが 8:00:00 に送られていて、
AWS SES の送信数制限に引っかかる可能性がより高かったと言えますね……。

4: ActionMailer 公式とメソッド名がかぶってしまう可能性

ほとんどない可能性ですが、
将来的に ActionMailer が deliver_avoid_midnight というメソッドを実装したときに、
そのメソッドが initializers によって上書きされて、ActionMailer 全体の挙動がおかしくなる可能性があります。

ActionMailer のリリースに注意しておく必要があるでしょう。

おわりに

以上、深夜のメール送信を防ぐ方法と、その実装によるデメリットでした!

デメリットが4つもあったように、
これがいい方法かというとけっこう怪しくて、ぜひ他の方法の知見をお持ちの方には教えていただきたいです!

深夜にメールを送らないのは、かんたんなようでいてなかなかに悩みますねー……。

ラブグラフのエンジニアブログ

Discussion