Код-ревью: рассылка уведомлений
Вопрос
Обработчик рассылает уведомления всем подписчикам. Найди проблемы с асинхронностью, нагрузкой и обработкой ошибок.
app.post('/notify', async (req, res) => {
const users = await db.query('SELECT * FROM users WHERE subscribed = true');
users.forEach(async (u) => {
await mailer.send(u.email, req.body.text);
});
res.json({ sent: users.length });
});
async function sendAll(list) {
const results = [];
for (const u of list) {
results.push(await mailer.send(u.email, 'hi'));
}
return Promise.all(results);
}
Коротко
Что должен найти ревьюер
- Производительностьстрока 2Все подписчики грузятся в память одним запросом и
SELECT *: на сотнях тысяч строк это память и время. Нужны нужные колонки и пагинация/курсор. - Ошибкастрока 3
forEachс async-колбэком не ждёт промисы: ответ уходит до отправки писем, ошибки отправки становятся unhandled rejection. - Производительностьстрока 4Если бы ждать все сразу, это тысячи одновременных запросов к SMTP: нужен лимит параллельности или очередь.
- Ошибкастрока 6Клиенту сообщают
sent, хотя ничего не отправлено. Долгую рассылку правильно ставить в очередь задач и отвечать 202 Accepted. - Безопасностьстрока 4
req.body.textуходит в письмо без валидации и без проверки прав: кто угодно может разослать что угодно всем. - Производительностьстрока 12
awaitв цикле делает отправку строго последовательной, аPromise.allв конце бессмыслен: массив уже содержит значения, а не промисы.
Разбор
Главная ошибка: forEach игнорирует возвращаемые промисы, поэтому обработчик отвечает сразу, а упавшие отправки превращаются в необработанные отклонения (в Node 15+ по умолчанию это падение процесса).
Как переписать по-взрослому: эндпоинт проверяет права и валидирует текст, кладёт задачу «рассылка» в очередь (BullMQ, RabbitMQ) и отвечает 202. Воркер читает подписчиков пачками по курсору и отправляет с ограничением параллельности и ретраями, записывая статус каждого письма, чтобы повтор задачи не слал письмо дважды.
Вторая функция показывает обратную крайность: последовательные await в цикле, где можно было бы отправлять пачками.
Ответьте сами, а не прочитайте
В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.
Предыдущий вопрос
withTimeout: дедлайн для промиса
Следующий вопрос
AsyncLocalStorage для контекста запроса