Цикл событий — Примеры
Пример 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
}
Ничто не вытесняет цикл, поэтому версия выше освобождает его каждую тысячу строк и даёт выполниться обратным вызовам, что ждут в очереди; для действительно тяжёлой работы ту же форму применяют с рабочим потоком или отдельным процессом. Лишняя память или процессоры циклу, который никогда не уступает управление, не помогут. И вот где ошибка: задержка выглядит как нехватка ёмкости, однако на деле её причина — ход цикла, который никак не заканчивается.