Множественные задержки в Javascript / Nodejs Promise - PullRequest
0 голосов
/ 26 мая 2020

Я работаю над прокси-сервером, который кэширует файлы, и я пытаюсь добавить лог c, который не позволяет нескольким клиентам загружать одни и те же файлы до того, как прокси-сервер сможет их кэшировать.

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

Клиент 1 запрашивает файл. Прокси-сервер проверяет, кэширован ли файл. Если это не так, он запрашивает его у сервера, кэширует, а затем отправляет клиенту.

Клиент 2 запрашивает тот же файл после того, как клиент 1 запросил его, но до того, как прокси-сервер получит возможность кэшировать его. Таким образом, прокси-сервер скажет клиенту 2 подождать несколько секунд, потому что загрузка уже выполняется.

Лучшим подходом, вероятно, было бы дать клиенту 2 сообщение «повторите попытку позже», но давайте просто скажем, что это в настоящее время не вариант.

Я использую Nodejs с библиотекой anyproxy . Согласно документации , отложенные ответы возможны при использовании обещаний.

Однако я действительно не вижу способа достичь того, чего я хочу, используя обещания. Насколько я могу судить, я мог бы сделать что-то вроде этого:

module.exports = {
  *beforeSendRequest(requestDetail) {
    if(thereIsADownloadInProgressFor(requestDetail.url)) {
        return new Promise((resolve, reject) => {
            setTimeout(() => { // delay
              resolve({ response: responseDetail.response });
            }, 10000);
        });
    }
  }
};

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

Я бы предпочел иметь возможность делать что-то вроде этого (но каким-то образом с помощью Promises):

module.exports = {
  *beforeSendRequest(requestDetail) {
    if(thereIsADownloadInProgressFor(requestDetail.url)) {
        var i = 0;
        for(i = 0 ; i < 10 ; i++) {
            JustSleep(1000);

            if(!thereIsADownloadInProgressFor(requestDetail.url))
                return { response: responseDetail.response };
        }
    }
  }
};

Есть ли способ добиться этого? с обещаниями в Nodejs?

Спасибо!

Ответы [ 2 ]

1 голос
/ 27 мая 2020

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

Вот реализация кеширования, которая выполняет только одну выборку для нескольких запросов. Без задержек и повторных попыток:

export class class Cache {    
    constructor() {
        this.resultCache = {}; // this object is the cache storage
    }

    async get(key, cachedFunction) {
        let cached = this.resultCache[key];

        if (cached === undefined) { // No cache so fetch data
            this.resultCache[key] = {
                pending: [] // This is the magic, store further
                            // requests in this pending array.
                            // This way pending requests are directly
                            // linked to this cache data
            }
            try {
                let result = await cachedFunction(); // Wait for result

                // Once we get result we need to resolve all pending
                // promises. Loop through the pending array and
                // resolve them. See code below for how we store pending
                // requests.. it will make sense:
                this.resultCache[key].pending
                    .forEach(waiter => waiter.resolve(result));

                // Store the result of the cache so later we don't
                // have to fetch it again:
                this.resultCache[key] = {
                    data: result
                }

                // Return result to original promise:
                return result;

                // Note: yes, this means pending promises will get triggered
                // before the original promise is resolved but normally
                // this does not matter. You will need to modify the
                // logic if you want promises to resolve in original order
            }
            catch (err) { // Error when fetching result

                // We still need to trigger all pending promises to tell
                // them about the error. Only we reject them instead of
                // resolving them:
                if (this.resultCache[key]) {
                    this.resultCache[key].pending
                      .forEach((waiter: any) => waiter.reject(err));
                }
                throw err;
            }
        }
        else if (cached.data === undefined && cached.pending !== undefined) {
            // Here's the condition where there was a previous request for
            // the same data. Instead of fetching the data again we store
            // this request in the existing pending array.
            let wait = new Promise((resolve, reject) => {

                // This is the "waiter" object above. It is basically
                // It is basically the resolve and reject functions
                // of this promise:
                cached.pending.push({
                    resolve: resolve,
                    reject: reject
                });
            });

            return await wait; // await response form original request.
                               // The code above will cause this to return.
        }
        else {
            // Return cached data as normal
            return cached.data;
        }
    }
}

Код может показаться немного сложным, но на самом деле он довольно прост. Сначала нам нужен способ хранения кэшированных данных. Обычно я бы использовал для этого обычный объект:

{ key : result }

Где кешированные данные хранятся в result. Но нам также необходимо хранить дополнительные метаданные, такие как ожидающие запросы с тем же результатом. Итак, нам нужно изменить наше хранилище кеша:

{ key : {
    data: result,
    pending: [ array of requests ]
  }
}

Все это невидимо и прозрачно для кода, использующего этот класс Cache.

Использование:

const cache = new Cache();

// Illustrated with w3c fetch API but you may use anything:
cache.get( URL , () => fetch(URL) )

Обратите внимание, что Обертывание выборки анонимной функцией важно, потому что мы хотим, чтобы функция Cache.get() вызывала выборку условно, чтобы избежать вызова множественной выборки. Это также дает классу Cache гибкость для обработки любых асинхронных операций.

Вот еще один пример кеширования setTimeout. Это не очень полезно, но демонстрирует гибкость API:

cache.get( 'example' , () => {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, 1000);
    });
});

Обратите внимание, что приведенный выше класс Cache не имеет недействительности или истечения срока действия logi c для ясности, но это довольно просто добавить их. Например, если вы хотите, чтобы срок действия кеша истекал через некоторое время, вы можете просто сохранить метку времени вместе с другими данными кеша:

{ key : {
    data: result,
    timestamp: timestamp,
    pending: [ array of requests ]
  }
}

Затем в логе «без кеша» c просто определите истечение срока время:

if (cached === undefined || (cached.timestamp + timeout) < now) ...
1 голос
/ 27 мая 2020

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

Отображение в Map будет url -> Promise { file }

// Map { url => Promise { file } }
const cache = new Map()

const thereIsADownloadInProgressFor = url => cache.has(url)

const getCachedFilePromise = url => cache.get(url)

const downloadFile = async url => {/* download file code here */}

const setAndReturnCachedFilePromise = url => {
  const filePromise = downloadFile(url)
  cache.set(url, filePromise)
  return filePromise
}

module.exports = {
  beforeSendRequest(requestDetail) {
    if(thereIsADownloadInProgressFor(requestDetail.url)) {
        return getCachedFilePromise(requestDetail.url).then(file => ({ response: file }))
    } else {
        return setAndReturnCachedFilePromise(requestDetail.url).then(file => ({ response: file }))
    }
  }
};
...