Код-ревью Middle Асинхронные паттерны обновлено

Код-ревью: рассылка уведомлений

Вопрос

Обработчик рассылает уведомления всем подписчикам. Найди проблемы с асинхронностью, нагрузкой и обработкой ошибок.

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);
}

Коротко

Главная ошибка: forEach игнорирует возвращаемые промисы, поэтому обработчик отвечает сразу, а упавшие отправки превращаются в необработанные отклонения (в Node 15+ по умолчанию это падение процесса).

Что должен найти ревьюер

  1. Производительностьстрока 2
    Все подписчики грузятся в память одним запросом и SELECT *: на сотнях тысяч строк это память и время. Нужны нужные колонки и пагинация/курсор.
  2. Ошибкастрока 3
    forEach с async-колбэком не ждёт промисы: ответ уходит до отправки писем, ошибки отправки становятся unhandled rejection.
  3. Производительностьстрока 4
    Если бы ждать все сразу, это тысячи одновременных запросов к SMTP: нужен лимит параллельности или очередь.
  4. Ошибкастрока 6
    Клиенту сообщают sent, хотя ничего не отправлено. Долгую рассылку правильно ставить в очередь задач и отвечать 202 Accepted.
  5. Безопасностьстрока 4
    req.body.text уходит в письмо без валидации и без проверки прав: кто угодно может разослать что угодно всем.
  6. Производительностьстрока 12
    await в цикле делает отправку строго последовательной, а Promise.all в конце бессмыслен: массив уже содержит значения, а не промисы.

Разбор

Главная ошибка: forEach игнорирует возвращаемые промисы, поэтому обработчик отвечает сразу, а упавшие отправки превращаются в необработанные отклонения (в Node 15+ по умолчанию это падение процесса).

Как переписать по-взрослому: эндпоинт проверяет права и валидирует текст, кладёт задачу «рассылка» в очередь (BullMQ, RabbitMQ) и отвечает 202. Воркер читает подписчиков пачками по курсору и отправляет с ограничением параллельности и ретраями, записывая статус каждого письма, чтобы повтор задачи не слал письмо дважды.

Вторая функция показывает обратную крайность: последовательные await в цикле, где можно было бы отправлять пачками.

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.

Термины по теме