Тайм-аут операций и защита от зависания

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

Каждый вызов генерации соли и вычисления хеша выполняется синхронно внутри одного потока выполнения JavaScript. Это означает прямое влияние на event loop: пока выполняется bcrypt-операция, цикл обработки событий блокируется, а сервер перестаёт обрабатывать другие запросы.

Ключевая особенность:

  • bcrypt.js не использует отдельные потоки
  • операции занимают десятки или сотни миллисекунд
  • при увеличении cost factor время растёт экспоненциально

Причины появления зависаний при хешировании

Зависания в контексте bcrypt.js не являются ошибкой в классическом смысле — это предсказуемое поведение CPU-bound задач в однопоточном рантайме Node.js.

Основные факторы:

1. Высокий cost factor (rounds) Каждое увеличение параметра rounds увеличивает количество итераций алгоритма. Разница между 10 и 14 может быть кратной по времени выполнения.

2. Пиковая нагрузка на API При одновременной регистрации или авторизации большого числа пользователей event loop блокируется множеством параллельных хеширований.

3. Синхронное использование API bcrypt.js предоставляет синхронные методы, которые полностью блокируют поток до завершения операции.


Ограничения тайм-аутов внутри bcrypt.js

Важно учитывать, что тайм-ауты на уровне JavaScript не могут прервать выполнение уже запущенной CPU-bound операции.

Пример распространённой ошибки:

setTimeout(() => {
  // попытка "остановить" bcrypt
}, 1000);

Если bcrypt уже выполняется, этот таймер не имеет возможности его прервать. Event loop занят, и callback выполнится только после завершения вычислений.

Даже конструкции вида Promise.race() не обеспечивают реальной остановки вычисления:

Promise.race([
  bcrypt.hash(password, 12),
  timeoutPromise(1000)
]);

Хеширование продолжит выполняться в фоне текущего потока.


Ограничение через Promise.race и его иллюзия контроля

Использование Promise.race часто применяется как попытка реализовать тайм-аут операций. Однако в случае bcrypt.js это создаёт лишь логическую отмену результата, но не останавливает сам процесс.

Побочные эффекты:

  • CPU продолжает быть загруженным
  • event loop остаётся заблокированным до завершения вычисления
  • сервер не получает освобождения ресурсов

Фактически это механизм контроля ответа, но не управления выполнением.


Изоляция bcrypt через worker threads

Наиболее корректный способ защиты от зависаний — вынесение bcrypt-операций в отдельные потоки.

Node.js предоставляет worker_threads, позволяющие выполнять CPU-heavy задачи вне основного event loop.

Пример архитектуры:

// worker.js
const { parentPort } = require('worker_threads');
const bcrypt = require('bcryptjs');

parentPort.on('message', async (data) => {
  const hash = await bcrypt.hash(data.password, data.rounds);
  parentPort.postMessage(hash);
});

Основной поток:

const { Worker } = require('worker_threads');

function hashPassword(password) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js');

    worker.postMessage({ password, rounds: 12 });

    worker.on('message', resolve);
    worker.on('error', reject);

    setTimeout(() => {
      worker.terminate();
      reject(new Error('bcrypt timeout'));
    }, 2000);
  });
}

Преимущества:

  • event loop остаётся свободным
  • можно безопасно ограничивать время выполнения
  • изоляция CPU-нагрузки

Очереди задач и контроль параллелизма

При высокой нагрузке запуск множества bcrypt-задач одновременно приводит к деградации производительности. Решением становится очередь задач с ограничением параллельных вычислений.

Типовая модель:

  • максимум N одновременных хеширований
  • остальные задачи ожидают в очереди
  • выполнение строго последовательно или батчами

Пример логики:

const PQueue = require('p-queue');
const bcrypt = require('bcryptjs');

const queue = new PQueue({ concurrency: 2 });

function safeHash(password) {
  return queue.add(() => bcrypt.hash(password, 12));
}

Эта модель предотвращает:

  • переполнение event loop
  • резкие пики CPU load
  • деградацию latency API

Тайм-ауты на уровне HTTP-сервера

Дополнительный уровень защиты реализуется через тайм-ауты самого сервера.

Express

app.use((req, res, next) => {
  req.setTimeout(2000);
  res.setTimeout(2000);
  next();
});

HTTP сервер Node.js

server.setTimeout(2000);

Ограничение на уровне HTTP не прерывает bcrypt напрямую, но позволяет:

  • разорвать соединение с клиентом
  • освободить сетевые ресурсы
  • предотвратить подвешенные запросы

Поведение при разрыве соединения

Даже если клиент отключился или истёк HTTP timeout, bcrypt продолжает выполнение, если он запущен в основном потоке.

Это ключевая проблема:

  • сеть освобождена
  • CPU продолжает работать
  • event loop остаётся заблокированным до завершения

Именно поэтому одного HTTP timeout недостаточно.


Оптимизация параметров cost factor

Выбор параметра rounds напрямую влияет на вероятность зависаний.

Практические наблюдения:

  • 8–10: низкая нагрузка, быстрые ответы
  • 11–12: баланс безопасности и производительности
  • 13+: значительная нагрузка, риск деградации под нагрузкой

Оптимизация зависит от:

  • ожидаемой нагрузки
  • аппаратных ресурсов
  • требований безопасности

Защита от деградации под нагрузкой

При работе с bcrypt.js важна комплексная защита системы:

1. Rate limiting Ограничение количества запросов на регистрацию/логин.

2. Backpressure Принудительное замедление входящего потока запросов при перегрузке очереди.

3. Изоляция CPU-bound задач bcrypt должен быть отделён от основного request pipeline.

4. Мониторинг event loop lag Рост задержек event loop указывает на блокировки:

  • увеличивающееся время ответа API
  • задержки других эндпоинтов без нагрузки

Практическая модель безопасной интеграции

Типовая устойчивая архитектура включает несколько уровней:

  • HTTP слой с тайм-аутами
  • очередь задач с ограничением параллелизма
  • worker threads для bcrypt
  • контроль нагрузки через rate limiting

Такой подход позволяет устранить основную проблему bcrypt.js — блокировку event loop при интенсивных вычислениях — без отказа от самого алгоритма хеширования.