Comments 21
Боролся, применял то же самое.
Есть ещё другой кейс гонки - когда нет смысла кэшировать, например там форма с каким-то набором значений, либо результат может меняться. Там проблема не в повторном запросе, а в том, что первый запрос, задержавшись в пути, может прийти после ответа на следующий запрос и накрыть своим устаревшим результатом. Там тоже стандартный подход: держать в замыкании колбэка "id запроса" и сравнивать с текущим актуальным id.
Как всё-таки мало в JavaScript проблем с многозадачностью... Не то что C++, где после четырёх callTimes++ в четырёх тредах не можешь быть уверен, чему будет равно значение callTimes.
ЗЫ: Хотя и кооперативная многозадачность может подкинуть сюрприз. Однажды столкнулся: отлаживаю код в Safari, нажимаю F6 (step over) – и отрабатывает код из очереди промисов. При том что никакого await не было, в главный цикл мы не попадали. Просто Safari решил, что остановка в отладчике – как раз подходящее время проверить очередь промисов.
А это необходимо для работы создавать функцию getOrSet в вызове метода или можно её вынести в отдельный метод?
Комментарий с примером кода почему то отклонили...
Вопрос такой, как вы будете решать задачу когда нода будет запускаться не в одном инстансе?
Не легче сразу прикрутить redlock если используете редис для Кеша? Ваше решение явно не production ready
Потому что в примере было не по теме. Вы решаете задачу как поднять несколько сервисов и синхронизировать между ними запись/чтение кеша Redis. В теме статьи, на примере работы с кешем, рассказывается о гонках и как с ними бороться. Безусловно, в этой статье я не расписываю много дополнительных деталей, поскольку они лишние для понимая проблемы.
Вы даёте заведомо неправильное решение, даже маленький проект в проде будут запускать минимум с пм2 в кластер моде, для чего тогда ваше решение?
Я даю решение как победить гонку. Пример призван продемонстрировать проблему. Мыслите шире, отвяжитесь от кешей и моего примера. У вас есть просто несколько параллельных вызовов асинхронного кода, в котором делаются вызовы другого асинхронного кода с await. Их можно решить тем же подходом - всем вызовам подсунуть один и тот же промис.
Статья не про синхронизацию кеша между разными процессами.
Я абсолютно абстрагирован, и говорю что для решения race condition в NodeJS нужно использовать блокировки, если речь не о работе с бд, то для этого используется distributed lock, редлок с редисом это один из возможных вариантов. Ваше решение - не решение, так как оно не работает реальном мире, где никто не крутит сервисы на ноде в одном инстансе
Кстати в яваскрипте нет параллельных вызовов метода. Все вызовы в этом языке последовательные, подменяете понятия и ещё больше запутываете читателя.
Лучше напишите как работают промисы и эвент луп
Не стоит отклонять комментарии просто потому, что вы с ними не согласны. ИМХО. Вот если человек какую-то грубость написал или ошибся статьёй, то другое дело.
По мне, так человек статьей ошибся. Я говорю в статье про шаблон, который при переносе из синхронного кода на асинхронный приводит к проблеме. Для демонстрации проблемы я выбрал приближенный к жизни пример.
Вот вам еще пример с тем же шаблоном, но без Redis.
4 раза вместо 1 создаем клиента и подключаемся к нему
class DummyClient {
async connect() {
return new Promise(resolve => {
setTimeout(() => {
console.log('connect');
resolve()
}, 2000);
})
}
async doSomething() {
console.log('done something')
}
}
class ApiWrapper {
#client;
async getClient() {
if (!this.#client) {
const client = new DummyClient();
await client.connect();
this.#client = client;
}
return this.#client;
}
async doSomething() {
const client = await this.getClient();
return client.doSomething();
}
}
const run = async () => {
const api = new ApiWrapper();
await Promise.all([api.doSomething(), api.doSomething(), api.doSomething(), api.doSomething()]);
}
run();
/* клиент создан 4 раза вместо одного */
По мне, так человек статьей ошибся.
Не, не ошибся. Я же вижу в его комментариях, как и в комментариях от mark_ablov, о чём идёт речь. Вы просто зря в статье про nodejs и redis написали. Там в общем случае ваш паттерн неуместен. В браузерной вкладке — ок, там всего 1 поток и гонки имеют скорее логический характер. Но когда много процессов вы так гонки не почините. Максимум замаскируете.
Пример не про race condition а про понимание как работают промисы
А комментарии автор что ли может отклонить?) Вот это новость
Кроме redlock'a можно и pubsub использовать, который предоставляется редисом из коробки. Правда там тоже нужно код аккуратно написать, дабы не нарваться на другие race condition'ы уже.
Как, если не секрет, тут прикрутить пабсаб?
Если в двух словах, то храним в redis'e не только кэш, но и флаг того что у нас есть inflight запрос.
const cached = await dbService.get(cacheKey(id));
if (cached) {
return cached;
}
// need to pre-subscribe, if we would subscribe after flag checking we can miss event
const fetchedEvent = eventForFetchedData(id);
const inflightPromise = new Promise((resolve) => {
messageBusService.sub(fetchedEvent, ({ data }) => {
messageBusService.unsub(fetchedEvent).catch();
resolve(data);
});
});
const fetchStartEvent = eventForFetchStart(vin);
const fetchStartPromise = new Promise((resolve) => {
messageBusService.sub(fetchStartEvent, ({ tag }) => {
messageBusService.unsub(fetchStartEvent).catch();
resolve(tag);
});
});
const inflightRequest = await dbService.get(cacheKeyForInflightFlag(id));
if (inflightRequest !== null) {
return inflightPricePromise;
}
const requesterId = getRequesterId();
await messageBusService.pub(fetchStartEvent, { tag: requesterId });
const firstRequesterId = await fetchStartPromise;
// we lost that race, some other thread was first to send message to message bus,
// that it is going to fetch data!
if (firstRequesterId !== requesterId) {
return inflightPromise;
}
messageBusService.unsub(fetchedEvent).catch();
const data = await callToAPI(id);
await dbService.set(cacheKey(id), data);
await dbService.del(cacheKeyForInflightFlag(id));
// order is important, we need to clean flag first, otherwise consumer can subscribe
// inbetween pub and del operations and get stuck
await messageBusService.pub(fetchedEvent, { data });
return data;
Забавно: мы похожую задачку даём на техническом интервью) Не всё же алгоритмами мучать - нужно что-то и из реального мира проверять.
Если говорить про блокировку на Redis для борьбы с гонкой, то у них в доках описан паттерн блокировки с использованием SETNX, просто и без redlock. Решение рабочее, я таким образом успешно ставлю на паузу 40 параллельных запросов в 4 инстанса, запущенных через pm2
Борьба с гонками (race conditions) в JavaScript на примере работы с кешем