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

Предположим, у вас есть служба, которая загружает данные из кеша, но при отсутствии результата возвращается к базе данных.

Наш первый подход может выглядеть примерно так:

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

Тогда наш вывод может выглядеть примерно так:

Data retrieved from database: nugget
Data retrieved from database: nugget
Data retrieved from database: hunk
Data retrieved from database: mass
{ results:
   [ 'nugget',
     'nugget',
     'hunk',
     'mass' ] }

Почему мы не попали в кеш для идентификатора, который мы уже запрашивали в базе данных?

В service.js все наши запросы приходят одновременно, и в этот начальный момент кэш еще не заполнен, это означает, что мы попадаем в оператор catch, где вместо этого данные извлекаются из базы данных.

Итак, следующее, что мы могли бы попробовать, — реализовать систему очередей для дедупликации запросов:

Если мы снова запустим consumer.js, мы получим:

Data retrieved from database: nugget
Data retrieved from database: nugget
Data retrieved from database: hunk
Data retrieved from database: mass
{ results:
   [ 'nugget',
     'nugget',
     'hunk',
     'mass' ] }

Тот же результат. Почему?

В dedupe.jsблоках await workToComplete до того, как queue[id] успеет застыть. Приходит следующий вызов, и в итоге повторяется то же условие, что приводит к промаху кеша.

Как сделать так, чтобы все последующие запросы ждали после первого?

Блин, это начинает усложняться.

При первом поступлении запроса работа выполняется и возвращается ссылка на промис. Выполнение активных блоков потока по ключевому слову await.

Когда поступает другой запрос с тем же идентификатором, возвращается первый промис, который позже выполняется с результатом первого.

Вывод при запуске consumer.js теперь выглядит так:

Performing work: nugget
Waiting for previous work to complete: nugget
Performing work: hunk
Performing work: mass
Data retrieved from database: nugget
Data retrieved from database: hunk
Work completed: nugget
Data retrieved from database: mass
Work completed: hunk
Work completed: mass
{ results:
   [ 'nugget',
     'nugget',
     'hunk',
     'mass' ] }

Мы видим, что работа выполняется (извлечение данных из базы данных) один раз для каждого идентификатора (т. е. самородка), а последующие запросы ждут завершения предыдущего (кэш в памяти).

Если мы снова запустим consumer.js, то увидим, что попадаем в кеш при выполнении работы и в кеш в памяти при последующих вызовах:

Performing work: nugget
Waiting for previous work to complete: nugget
Performing work: hunk
Performing work: mass
Data retrieved from cache: nugget
Work completed: nugget
Data retrieved from cache: hunk
Work completed: hunk
Data retrieved from cache: mass
Work completed: mass
{ results:
   [ 'nugget',
     'nugget',
     'hunk',
     'mass' ] }

Функционально это дает нам то, что мы намеревались сделать, однако мы можем улучшить это дальше:

Используя Map, мы удаляем кучу null значений, которые создаются, когда обещание выполнено или отклонено, и перемещаем код, чтобы мы могли вернуться раньше, это делает код более сжатым и немного более легким для понимания.

Спасибо следующим людям: Виктории Фивер, Оливеру Рамбелоу, Люку Джонсу.