【反省】バッチの実装を誤って、盛大にご迷惑をおかけしてしまった話
やらかしてしまいました。今回は業務における失敗談をお話しようと思います。
何をやらかしたか
バッチの実装を誤った結果多数のユーザーにメールを重複送信してしまい、ユーザーはもちろん社内のカスタマーサクセスチーム、開発チームにもご迷惑をおかけしてしまいました。
技術スタック
- typescript
- Prisma
- Mysql
バッチの要件
特定のステータスを持つユーザーに対して、以下の処理を行うバッチです
- ステータスの更新 ※更新すること次のバッチの対象から外れる
- その他の処理 ※本題から逸れるので省きます
- メール送信を行う
どんな実装をしたか
流れとしてはこんな感じ
- バッチの対象ユーザーを取得
- ユーザーに対して、updateMany等で一括更新処理
- 処理が終わったら対象ユーザーにメール送信
export async function sample() {
// 対象ユーザーを取得
const targetUsers = await UserRepository.findMany()
if (targetUsers.length === 0) return
const targetUserIds = targetUsers.map((user) => user.id))
await prisma.$transaction(async (tx) => {
// 一括更新処理 ※これにより当該ユーザーは次回のバッチ処理対象ではなくなる
await UserRepository.updateMany({
where: { id: { in: targetUserIds } },
data: {
...
}
})
// その他の処理
...(省略)
// メール送信
for (const user of targetUsers) {
try {
await sendMail({
to: user.email,
})
} catch (e) {
// ログ出力
}
}
})
}
なぜメールが重複送信されたか
結論から言うと「トランザクションタイムアウトが発生し、データの更新はロールバックされたものの、メールは送信されたため」です。
ロールバックされる=データが更新されないので、次回のバッチ実行時にも同じユーザーが処理の対象となってしまい、結果として同じユーザーに何度もメールが送信され続けてしまう状態を引き起こしてしまいました。
なぜトランザクションタイムアウトが起きたか
- トランザクションの中身はそれほど重い処理ではない
- このバッチは1分周期で実行される
- 対象ユーザー数は通常1桁人程度
上記の想定で作成したバッチでしたが、今回は諸々の事情が重なって200人程度のユーザーが対象となってしまい、タイムアウトにつながりました。
何がダメだったか
メール送信処理をトランザクションに含めた
明らかに、メール送信処理をトランザクションに含めたのが間違いです。
トランザクションはDBをロールバックするものであって、メール送信やS3の画像削除を取り消せるわけじゃないですから。
もちろんそういった認識(意図)で上記の実装をしてたわけではなく、単純に確認漏れ、凡ミスです。
あるいは、「メール送信に失敗したらDBをロールバックしたい」という意図を持った実装にも見えると思います。
今回はそういった要件はないのですが、もしそのような要件を満たすのであれば、updateManyで対象ユーザーを一括更新するのではなく、ユーザーをループして1件ずつDB更新+その他の処理+メール送信を行う実装にすべきです。
各処理に失敗したら、ログを吐かせることも忘れずに。
本番での対象ユーザー数を正しく考慮できていなかった
既述の通り、通常時は対象ユーザー数は1桁人であるため、無意識に「トランザクションタイムアウトが起こるはずがない...」と考えてしまっていたのだと思います。
しかし実際、本番環境では200人程度のユーザーが対象となり、今回の事象に繋がりました。
ユーザーが一時的に200人程度になることは事前に把握することが可能でしたので、「本番環境で動かしたらどうなるか?」の想定を怠ったことが原因の一つだと思っています。
実装時、レビュー時、STG環境でのテスト時、気づけるタイミングはたくさんありましたが、見逃してしましました。実装時に気づけなくても、テスト時に「念の為、数百人程度のユーザーで試してみよう」となってれば、本番でエラーを出さずに済みました。
落ち込んだ
個人的に、こういった凡ミスをするとすごく凹みます。
こんなミスをしてしまう自分の至らなさが悔しいですし、またそれ以上に、今回は数百人以上のユーザーに重複メールを出してしまい、CSチームや開発チームのメンバーにもご迷惑をおかけしたわけなので、申し訳なく思います。
- 「なんでこんなことに気づけなかったんだ...」
- 「普段なら気づけてたはずなのに...」
- 「他の作業も立て込んでて、忙しかったからかな...」
とか色々思うんですが、結局 実装・テストをしても見落としていたわけなのでこれが実力なんですよね。忙しいタイミングなんていくらでもありますし、ユーザーや他チームからしたらそんなこと関係ないですし、どんな状況であれ良い仕事をしないといけませんね。
時間に余裕がないとどうしても焦ってしまって、あらゆる作業が雑になりがちです。自分の悪い癖なんですが、なかなか治せません😓
あるいは、忙しくてクオリティが落ちてしまうなら、それは忙しい状況を作ってしまった時点で負けなんだとも思います。
Discussion