Цикл событий — Примеры

Пример 1 — Синхронное чтение на пути запроса

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

import { createServer } from 'node:http'
import { readFileSync } from 'node:fs'

createServer((req, res) => {
  if (req.url === '/report') {
    const config = readFileSync('./report.json', 'utf8')   // ждёт здесь
    res.end(render(config))
  } else {
    res.end('ok')
  }
}).listen(3000)

Решение

import { createServer } from 'node:http'
import { readFile } from 'node:fs/promises'

createServer(async (req, res) => {
  if (req.url !== '/report') return res.end('ok')
  try {
    const config = await readFile('./report.json', 'utf8')
    res.end(render(config))
  } catch (err) {
    res.writeHead(500).end('отчёт недоступен')
  }
}).listen(3000)

Асинхронный вызов регистрирует чтение и сразу возвращает управление, поэтому, пока приходят байты, цикл успевает обслужить проверку работоспособности, а путь ошибки превращается в обычный ответ 500 вместо необработанной ошибки, которая завершает процесс. Обычная ошибка — оставить синхронную форму из-за короткого обработчика и увидеть её цену лишь тогда, когда в работе окажется второй запрос.

Пример 2 — Вычисление, замораживающее каждый запрос

Эндпоинт подсчёта обходит большой массив вообще без await. Пока он работает, ждёт каждое другое соединение, и ни дополнительная память, ни реплики ничего не изменили. Разбейте работу так, чтобы цикл успевал переключаться между кусками.

function scoreAll(rows) {
  let total = 0
  for (const row of rows) {
    total += heavyScore(row)      // пока это не вернётся, больше ничего не выполняется
  }
  return total
}

Решение

const yieldToLoop = () => new Promise((resolve) => setTimeout(resolve, 0))

async function scoreAll(rows) {
  let total = 0
  for (let i = 0; i < rows.length; i++) {
    total += heavyScore(rows[i])
    if (i % 1000 === 0) await yieldToLoop()
  }
  return total
}

Ничто не вытесняет цикл, поэтому версия выше освобождает его каждую тысячу строк и даёт выполниться обратным вызовам, что ждут в очереди; для действительно тяжёлой работы ту же форму применяют с рабочим потоком или отдельным процессом. Лишняя память или процессоры циклу, который никогда не уступает управление, не помогут. И вот где ошибка: задержка выглядит как нехватка ёмкости, однако на деле её причина — ход цикла, который никак не заканчивается.