Шаблоны Promise Retry Design

редактировать

  1. Шаблон, который продолжает повторяться до разрешения обещания (с задержкой и maxRetries).
  2. Шаблон, который продолжает повторяться до тех пор, пока условие не будет соответствовать результату (с задержкой и maxRetries).
  3. Эффективный по памяти динамический шаблон с неограниченным количеством повторов (задержка при условии).

Код для № 1. Продолжает повторять попытки до тех пор, пока обещание не разрешится (какие-либо улучшения сообщества для языка и т.д.?)

Promise.retry = function(fn, times, delay) {
    return new Promise(function(resolve, reject){
        var error;
        var attempt = function() {
            if (times == 0) {
                reject(error);
            } else {
                fn().then(resolve)
                    .catch(function(e){
                        times--;
                        error = e;
                        setTimeout(function(){attempt()}, delay);
                    });
            }
        };
        attempt();
    });
};

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

work.getStatus()
    .then(function(result){ //retry, some glitch in the system
        return Promise.retry(work.unpublish.bind(work, result), 10, 2000);
    })
    .then(function(){console.log('done')})
    .catch(console.error);

Код № 2 повторять до тех пор, пока не будет выполнено условие then результат в многократном использовании (условие, что будет меняться).

work.publish()
    .then(function(result){
        return new Promise(function(resolve, reject){
            var intervalId = setInterval(function(){
                work.requestStatus(result).then(function(result2){
                    switch(result2.status) {
                        case "progress": break; //do nothing
                        case "success": clearInterval(intervalId); resolve(result2); break;
                        case "failure": clearInterval(intervalId); reject(result2); break;
                    }
                }).catch(function(error){clearInterval(intervalId); reject(error)});
            }, 1000);
        });
    })
    .then(function(){console.log('done')})
    .catch(console.error);

23 ответа

Решение

Что-то немного другое...

Асинхронные повторы могут быть достигнуты путем создания .catch() цепь, в отличие от более обычной .then() цепь.

Этот подход:

  • возможно только с указанным максимальным количеством попыток. (Цепь должна быть конечной длины),
  • рекомендуется только с низким максимумом. (Цепочки обещаний потребляют память, примерно пропорциональную их длине).

В противном случае используйте рекурсивное решение.

Во-первых, функция полезности, которая будет использоваться в качестве .catch() Перезвоните.

var t = 500;

function rejectDelay(reason) {
    return new Promise(function(resolve, reject) {
        setTimeout(reject.bind(null, reason), t); 
    });
}

Теперь вы можете создавать цепочки.catch очень кратко:

1. Повторите попытку, пока обещание не будет выполнено, с задержкой.

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
    p = p.catch(attempt).catch(rejectDelay);
}
p = p.then(processResult).catch(errorHandler);

ДЕМО: https://jsfiddle.net/duL0qjqe/

2. Повторите попытку, пока результат не встретит некоторое условие, без задержки

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
    p = p.catch(attempt).then(test);
}
p = p.then(processResult).catch(errorHandler);

ДЕМО: https://jsfiddle.net/duL0qjqe/1/

3. Повторите попытку, пока результат не встретит некоторое условие, с задержкой

Обдумав (1) и (2), комбинированный тест + задержка одинаково тривиален.

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
    p = p.catch(attempt).then(test).catch(rejectDelay);
    // Don't be tempted to simplify this to `p.catch(attempt).then(test, rejectDelay)`. Test failures would not be caught.
}
p = p.then(processResult).catch(errorHandler);

test() может быть синхронным или асинхронным.

Также было бы тривиально добавить дополнительные тесты. Просто вставьте цепочку между двумя защелками.

p = p.catch(attempt).then(test1).then(test2).then(test3).catch(rejectDelay);

ДЕМО: https://jsfiddle.net/duL0qjqe/3/


Все версии предназначены для attempt быть возвращающей обещание асинхронной функцией. Он также может предположительно вернуть значение, и в этом случае цепочка будет следовать по пути успеха к следующему / терминалу. .then(),

2. Шаблон, который продолжает повторяться до тех пор, пока условие не будет соответствовать результату (с задержкой и maxRetries).

Это хороший способ сделать это с родными обещаниями рекурсивным способом:

const wait = ms => new Promise(r => setTimeout(r, ms));

const retryOperation = (operation, delay, times) => new Promise((resolve, reject) => {
  return operation()
    .then(resolve)
    .catch((reason) => {
      if (times - 1 > 0) {
        return wait(delay)
          .then(retryOperation.bind(null, operation, delay, times - 1))
          .then(resolve)
          .catch(reject);
      }
      return reject(reason);
    });
});

Вот как вы это называете, предполагая, что func иногда успешно, а иногда и не всегда, возвращая строку, которую мы можем записать:

retryOperation(func, 1000, 5)
  .then(console.log)
  .catch(console.log);

Здесь мы вызываем retryOperation и просим его повторять каждую секунду, а max retries = 5.

Если вы хотите что-то более простое без обещаний, RxJs помогут с этим: https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/retrywhen.md

Упоминается много хороших решений, и теперь с помощью async/await эти проблемы могут быть решены без особых усилий.

Если вы не возражаете против рекурсивного подхода, то это мое решение.

function retry(fn, retries=3, err=null) {
  if (!retries) {
    return Promise.reject(err);
  }
  return fn().catch(err => {
      return retry(fn, (retries - 1), err);
    });
}

Вы можете связать новое обещание с предыдущим, откладывая его окончательное решение до тех пор, пока не узнаете окончательный ответ. Если следующий ответ по-прежнему неизвестен, тогда добавьте к нему еще одно обещание и продолжайте цепочку checkStatus() до тех пор, пока в конце концов вы не узнаете ответ и не сможете вернуть окончательное решение. Это может работать так:

function delay(t) {
    return new Promise(function(resolve) {
        setTimeout(resolve, t);
    });
}

function checkStatus() {
    return work.requestStatus().then(function(result) {
        switch(result.status) {
            case "success":
                return result;      // resolve
            case "failure":
                throw result;       // reject
            case default:
            case "inProgress": //check every second
                return delay(1000).then(checkStatus);
        }
    });
}

work.create()
    .then(work.publish) //remote work submission
    .then(checkStatus)
    .then(function(){console.log("work published"})
    .catch(console.error);

Обратите внимание, я также избегал создавать обещания вокруг вашего switch заявление. Так как вы уже в .then() обработчик, просто возвращение значения является решением, создание исключения - отклонением, а возвращение обещания связывает новое обещание с предыдущим. Это охватывает три ветви вашего switch заявление без создания нового обещания там. Для удобства я использую delay() функция, которая основана на обещании.

К вашему сведению, это предполагает work.requestStatus() не нужно никаких аргументов. Если для этого нужны конкретные аргументы, вы можете передать их в точке вызова функции.


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

function delay(t) {
    return new Promise(function(resolve) {
        setTimeout(resolve, t);
    });
}

function checkStatus(timeout) {
    var start = Date.now();

    function check() {
        var now = Date.now();
        if (now - start > timeout) {
            return Promise.reject(new Error("checkStatus() timeout"));
        }
        return work.requestStatus().then(function(result) {
            switch(result.status) {
                case "success":
                    return result;      // resolve
                case "failure":
                    throw result;       // reject
                case default:
                case "inProgress": //check every second
                    return delay(1000).then(check);
            }
        });
    }
    return check;
}

work.create()
    .then(work.publish) //remote work submission
    .then(checkStatus(120 * 1000))
    .then(function(){console.log("work published"})
    .catch(console.error);

Я не уверен, какой именно "шаблон дизайна" вы ищете. Так как вы, кажется, возражаете против внешне заявленного checkStatus() функция, вот встроенная версия:

work.create()
    .then(work.publish) //remote work submission
    .then(work.requestStatus)
    .then(function() {
        // retry until done
        var timeout = 10 * 1000;
        var start = Date.now();

        function check() {
            var now = Date.now();
            if (now - start > timeout) {
                return Promise.reject(new Error("checkStatus() timeout"));
            }
            return work.requestStatus().then(function(result) {
                switch(result.status) {
                    case "success":
                        return result;      // resolve
                    case "failure":
                        throw result;       // reject
                    case default:
                    case "inProgress": //check every second
                        return delay(1000).then(check);
                }
            });
        }
        return check();
    }).then(function(){console.log("work published"})
    .catch(console.error);

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


Вот еще один подход, который использует .retryUntil() метод на Promise.prototype по вашему запросу. Если вы хотите настроить детали реализации этого, вы сможете изменить этот общий подход:

// fn returns a promise that must be fulfilled with an object
//    with a .status property that is "success" if done.  Any
//    other value for that status means to continue retrying
//  Rejecting the returned promise means to abort processing 
//        and propagate the rejection
// delay is the number of ms to delay before trying again
//     no delay before the first call to the callback
// tries is the max number of times to call the callback before rejecting
Promise.prototype.retryUntil = function(fn, delay, tries) {
    var numTries = 0;
    function check() {
        if (numTries >= tries) {
            throw new Error("retryUntil exceeded max tries");
        }
        ++numTries;
        return fn().then(function(result) {
            if (result.status === "success") {
                return result;          // resolve
            } else {
                return Promise.delay(delay).then(check);
            }
        });
    }
    return this.then(check);
}

if (!Promise.delay) {
    Promise.delay = function(t) {
        return new Promise(function(resolve) {
            setTimeout(resolve, t);
        });
    }
}


work.create()
    .then(work.publish) //remote work submission
    .retryUntil(function() {
        return work.requestStatus().then(function(result) {
            // make this promise reject for failure
            if (result.status === "failure") {
                throw result;
            }
            return result;
        })
    }, 2000, 10).then(function() {
        console.log("work published");
    }).catch(console.error);

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

work.create()
    .then(work.publish) //remote work submission
    .then(function() {
        var tries = 0, maxTries = 20;
        function next() {
            if (tries > maxTries) {
                throw new Error("Too many retries in work.requestStatus");
            }
            ++tries;
            return work.requestStatus().then(function(result) {
                switch(result.status) {
                    case "success":
                        return result;
                    case "failure":
                        // if it failed, make this promise reject
                        throw result;
                    default:
                        // for anything else, try again after short delay
                        // chain to the previous promise
                        return Promise.delay(2000).then(next);
                }

            });
        }
        return next();
    }).then(function(){
        console.log("work published")
    }).catch(console.error);

Вот экспоненциальная реализация повторных попыток отката с использованием async/await, которая может обернуть любой обещающий API. Он симулирует нестабильную конечную точку с математической случайностью, поэтому попробуйте несколько раз, чтобы увидеть как успехи, так и неудачи.

/**
 * Wrap a promise API with a function that will attempt the promise over and over again
 * with exponential backoff until it resolves or reaches the maximum number of retries.
 *   - First retry: 500 ms + <random> ms
 *   - Second retry: 1000 ms + <random> ms
 *   - Third retry: 2000 ms + <random> ms
 * and so forth until maximum retries are met, or the promise resolves.
 */
const withRetries = ({ attempt, maxRetries }) => async (...args) => {
  const slotTime = 500;
  let retryCount = 0;
  do {
    try {
      console.log('Attempting...', Date.now());
      return await attempt(...args);
    } catch (error) {
      const isLastAttempt = retryCount === maxRetries;
      if (isLastAttempt) {
        // Stack Overflow console doesn't show unhandled
        // promise rejections so lets log the error.
        console.error(error);
        return Promise.reject(error);
      }
    }
    const randomTime = Math.floor(Math.random() * slotTime);
    const delay = 2 ** retryCount * slotTime + randomTime;
    // Wait for the exponentially increasing delay period before retrying again.
    await new Promise(resolve => setTimeout(resolve, delay));
  } while (retryCount++ < maxRetries);
}

const fakeAPI = (arg1, arg2) => Math.random() < 0.25 ? Promise.resolve(arg1) : Promise.reject(new Error(arg2))
const fakeAPIWithRetries = withRetries({ attempt: fakeAPI, maxRetries: 3 });
fakeAPIWithRetries('arg1', 'arg2').then(results => console.log(results))

Проверьте @ jsier / retrier. Протестировано, задокументировано, легковесно, просто в использовании, без внешних зависимостей и уже довольно давно в производстве.

Поддерживает:

  • Задержка первой попытки
  • Задержка между попытками
  • Ограничение количества попыток
  • Обратный вызов для остановки повторных попыток, если выполняется какое-либо условие (например, обнаружена конкретная ошибка)
  • Обратный вызов для продолжения повторной попытки, если выполняется какое-то условие (например, разрешенное значение неудовлетворительно)

Установка:

npm install @jsier/retrier --save

Применение:

import { Retrier } from '@jsier/retrier';

const options = { limit: 5, delay: 2000 };
const retrier = new Retrier(options);
retrier
  .resolve(attempt => new Promise((resolve, reject) => reject('Dummy reject!')))
  .then(
    result => console.log(result),
    error => console.error(error) // After 5 attempts logs: "Dummy reject!"
  );

Пакет не имеет внешних зависимостей.

Вот моя попытка. Я попытался извлечь из всех вышеперечисленных ответов то, что мне понравилось. Никаких внешних зависимостей. Машинопись + async / await (ES2017)

export async function retryOperation<T>(
  operation: () => (Promise<T> | T), delay: number, times: number): Promise<T> {
    try {
      return await operation();
    } catch (ex) {
      if (times > 1) {
        await new Promise((resolve) => setTimeout(resolve, delay));
        return retryOperation(operation, delay, times - 1);
      } else {
        throw ex;
      }
    }
}

Применение:

function doSomething() {
  return Promise.resolve('I did something!');
}

const retryDelay = 1000; // 1 second
const retryAttempts = 10;


retryOperation(doSomething(), retryDelay, retryAttempts)
    .then((something) => console.log('I DID SOMETHING'))
    .catch((err) => console.error(err));

Основываясь на решении Холмберда с немного более чистым кодом и задержкой

const wait = seconds => new Promise((resolve) => {
  setTimeout(() => resolve(), seconds * 1000)
})


const retryWithDelay = async (
  fn, retries = 3, interval = 50, 
  finalErr = Error('Retry failed')
) => fn().catch(async () => {
  if (retries <= 0) {
    return Promise.reject(finalErr);
  }
  await wait(interval)
  return retryWithDelay(fn, (retries - 1), interval, finalErr);
}

}

Если ваш код помещен в класс, вы можете использовать для этого декоратор. Такой декоратор у вас есть в утилитах-декораторах (npm install --save utils-decorators):

import {retry} from 'utils-decorators';

class SomeService {

   @retry(3)
   doSomeAsync(): Promise<any> {
    ....
   }
}

Примечание: эта библиотека может быть изменена по дереву, поэтому вы не будете платить лишние байты за остальные доступные декораторы в этой библиотеке.

https://github.com/vlio20/utils-decorators

Не уверен, почему все предлагаемые решения рекурсивны. Итеративное решение с TypeScript, которое ждет, пока метод не вернет что-то, что не является неопределенным:

function DelayPromise(delayTime): Promise<void> {
  return new Promise<void>((resolve) => setTimeout(resolve, delayTime));
}

interface RetryOptions {
  attempts?: number;
  delayMs?: number;
}

export async function retryOperation<T>(
  operation: (attempt: number) => Promise<T>,
  options: RetryOptions = {}
): Promise<T> {
  const { attempts = 6, delayMs = 10000 } = options;
  for (let i = 0; i < attempts; i++) {
    const result = await operation(i);
    if (typeof result !== 'undefined') {
      return result;
    }
    await DelayPromise(delayMs);
  }
  throw new Error('Timeout');
}

Здесь есть много ответов, но после некоторых исследований я решил пойти с рекурсивным подходом. Я оставляю свое решение здесь для всех, кого интересует

function retry(fn, retriesLeft = 2, interval = 1000) {
  return new Promise((resolve, reject) => {
    fn()
      .then(resolve)
      .catch((error) => {
        if (retriesLeft === 0) {
          reject(error);
          return;
        }

        setTimeout(() => {
          console.log('retrying...')
          retry(fn, retriesLeft - 1, interval).then(resolve).catch(reject);
        }, interval);
      });
  });
}

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

https://js-vjramh.stackblitz.io/

Вот мое решение:

  • Сохраняйте тип функции с помощью Typescript.
  • Принять функцию с любыми параметрами.
  • Настроить количество maxRetries.
  • Настройка поведения задержки
      type AnyFn = (...any: any[]) => any;
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
type DelayFn = (retry: number) => number;

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export function retry<Fn extends AnyFn>(
  fn: Fn,
  maxRetries: number,
  getDelay: DelayFn = () => 5000
) {
  let retries = 0;

  return async function wrapped(
    ...args: Parameters<Fn>
  ): Promise<Awaited<ReturnType<Fn>>> {
    try {
      return await fn(...args);
    } catch (e) {
      if (++retries > maxRetries) throw e;

      const delayTime = getDelay(retries);
      console.error(e);
      console.log(`Retry ${fn.name} ${retries} times after delaying ${delayTime}ms`);
      await delay(delayTime);
      return await wrapped(...args);
    }
  };
}

Применение

      const badFn = () => new Promise((resolve, reject) => reject('Something is wrong');
const fn = retry(badFn, 5, (retry) => 2 ** retry * 1000);

fn();

// Something is wrong
// Retry badFn 1 times after delaying 2000ms
// Something is wrong
// Retry badFn 2 times after delaying 4000ms
// Something is wrong
// Retry badFn 3 times after delaying 8000ms
// Something is wrong
// Retry badFn 4 times after delaying 16000ms
// Something is wrong
// Retry badFn 5 times after delaying 32000ms

async-retry.ts пытается реализовать шаблон, я использую его в производстве для некоторых проектов.

Монтаж:

npm установить async-retry.ts --save

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

import Action from 'async-retry.ts'

const action = async()=>{}
const handlers = [{
  error: 'error1',
  handler: async yourHandler1()=>{}
}, {
  error: 'error2',
  handler: async yourHandler2()=>{}
}]

await Action.retryAsync(action, 3, handlers)

Этот пакет довольно новый, но он получен из долгоживущего пакета co-retry который реализовал retry pattern в моде функции генератора.

function TryToSuccess(fun, reties) {
    let attempt = 0;

    let doTry = (...args) => {
        attempt++;
        return fun(...args)
                .catch((err) => {
                    console.log("fail ", attempt);
                    if(attempt <= reties){
                        return doTry(...args);
                    } else {
                        return Promise.reject(err);
                    }
                });
    }

    return doTry;
}

function asyncFunction(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            (window.findResult === true) ? resolve("Done") : reject("fail");
        }, 2000);
    });
}

var cloneFunc = TryToSuccess(asyncFunction, 3);

cloneFunc()
    .then(res => { 
        console.log("Got Success. ", res)
    })
    .catch(err => { 
        console.log("Rejected with err ", err); 
    });

setTimeout(() => {
    window.findResult = true;
}, 4000);

Это отлично работает для меня:

      async wait(timeInMilliseconds: number, name?: string) {
    const messageSuffix = name ? ` => ${name}` : ""
    await this.logger.info(`Waiting for ${timeInMilliseconds} ms${messageSuffix}`).then(log => log())
    return new Promise<void>(resolve => setTimeout(resolve, timeInMilliseconds))
}

async waitUntilCondition(name: string, condition: () => boolean | Promise<boolean>, scanTimeInSeconds: number, timeoutInSeconds: number) {
    await this.logger.info(`Waiting until condition: name=${name}, scanTime: ${scanTimeInSeconds} s, timeout: ${timeoutInSeconds} s`).then(log => log())
    const timeoutInMillis = timeoutInSeconds * 1000
    return new Promise<void>(async (resolve, reject) => {
        const startTime = new Date().getTime()
        let completed = false
        let iteration = 0
        while (!completed) {
            if (iteration++ > 0) {
                const timingOutInSeconds = Math.round((timeoutInMillis - (new Date().getTime() - startTime)) / 1000.0)
                await this.wait(scanTimeInSeconds * 1000, `${name}, timing out in ${timingOutInSeconds} s`)
            }
            try {
                completed = await condition()
                if (completed) {
                    resolve()
                    return
                }
            } catch (error: any) {
                reject(error)
                throw error
            }
            const waitTimeMillis = new Date().getTime() - startTime
            if (waitTimeMillis > timeoutInMillis) {
                reject(`The condition '${name}' timed out. Time waited: ${waitTimeMillis / 1000} seconds`)
                return
            }
        }
    })
}
work.create()
    .then(work.publish) //remote work submission
    .then(function(result){
        var maxAttempts = 10;
        var handleResult = function(result){
            if(result.status === 'success'){
                return result;
            }
            else if(maxAttempts <= 0 || result.status === 'failure') {
                return Promise.reject(result);
            }
            else {
                maxAttempts -= 1;
                return (new Promise( function(resolve) {
                    setTimeout( function() {
                        resolve(_result);
                    }, 1000);
                })).then(function(){
                    return work.requestStatus().then(handleResult);
                });
            }
        };
        return work.requestStatus().then(handleResult);
    })
    .then(function(){console.log("work published"})
    .catch(console.error);

Я даю вам решение async/await, получайте удовольствие:)

async function scope() {

  /* Performs an operation repeatedly at a given frequency until
     it succeeds or a timeout is reached and returns its results. */
  async function tryUntil(op, freq, tout) {
    let timeLeft = tout;
    while (timeLeft > 0) {
      try {
        return op();
      } catch (e) {
        console.log(timeLeft + " milliseconds left");
        timeLeft -= freq;
      }
      await new Promise((resolve) => setTimeout(() => resolve(), freq));
    }
    throw new Error("failed to perform operation");
  }

  function triesToGiveBig() {
    const num = Math.random();
    if (num > 0.95) return num;
    throw new Error();
  }

  try {
    console.log(await tryUntil(triesToGiveBig, 100, 1000));
  } catch (e) {
    console.log("too small :(");
  }

}

scope();

Мое решение для TypeScript:

export const wait = (milliseconds: number): Promise<void> =>
  new Promise(resolve => {
    setTimeout(() => resolve(), milliseconds);
  });

export const retryWithDelay = async (
  fn,
  retries = 3,
  interval = 300
): Promise<void> =>
  fn().catch(async error => {
    if (retries <= 0) {
      return Promise.reject(error);
    }
    await wait(interval);
    return retryWithDelay(fn, retries - 1, interval);
  });

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

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

На практике я обнаружил, что не следует предполагать, что операцию всегда можно повторить, и общийretryпомощнику нужен способ делегировать эту проверку вызывающему коду более высокого уровня. Пример ниже показывает, что сработало для меня.

      
/* The retry function takes a function to invoke, and a set 
 * of optional parameters to control the delay between retries 
 * (no backoff algorithm implemented here, but other example 
 * show how you might add that one), how many times to attempt
 * retrying and also a way to check if a retry should be 
 * attempted.
 *
 * And it returns a Promise that can be used in promise-
 * chaining and other async patterns.
 *
 */
const retry = (fn, 
               ms = 1000, 
               maxRetries = 2, 
               fnRetryable) => new Promise((resolve, reject) => {

  var retries = 0;

  if(!fnRetryable) {
    // default to always retryable
    fnRetryable = function() { return true };
  }

  fn()
  .then(resolve)
  .catch((err) => {
    if(!fnRetryable(err)) {
      return reject('Non-retryable');
    } else {
      setTimeout(() => {
        ++retries;
        if(retries == maxRetries) {
          return reject('Max retries exceeded');
        }
        retry(fn, ms).then(resolve);
      }, ms);
    }
  })
});

function doFoo(opts) {
  // Return a Promise that resolves after doing something with opts
  // or rejects with err.statusCode
}

function doFooWithRetry(opts, ms = 1000, maxRetries = 2) {
  var attempt = function() {
    return doFoo(opts);
  }
  var retryable = function(err) {
    // example, retry on rate limit error
    if(err && err.statusCode == 429) {
      return true;
    } else {
      return false;
    }
  }

  return retry(attempt, ms, maxRetries, retryable);
}

На всякий случай кто-то ищет более общее решение. Вот мои два цента:

Вспомогательная функция:

/**
 * Allows to repeatedly call
 * an async code block
 *
 * @callback callback
 * @callback [filterError] Allows to differentiate beween different type of errors
 * @param {number} [maxRetries=Infinity]
 */
function asyncRetry(
  callback,
  { filterError = (error) => true, maxRetries = Infinity } = {}
) {
  // Initialize a new counter:
  let tryCount = 0;
  // Next return an async IIFY that is able to
  // call itself recursively:
  return (async function retry() {
    // Increment out tryCount by one:
    tryCount++;
    try {
      // Try to execute our callback:
      return await callback();
    } catch (error) {
      // If our callback throws any error lets check it:
      if (filterError(error) && tryCount <= maxRetries) {
        // Recursively call this IIFY to repeat
        return retry();
      }
      // Otherwise rethrow the error:
      throw error;
    }
  })();
}

Демо

Попробуйте 2 раза:

await asyncRetry(async () => {
  // Put your async code here
}, { maxRetries = 2 })

Попробуйте 2 раза и повторите попытку DOMErrors:

await asyncRetry(async () => {
  // Put your async code here
}, { 
  maxRetries = 2,
  filterError: (error) => error instance of DOMError
})

Infine Retry: (Не делайте этого!)

await asyncRetry(async () => {
  // Put your async code here
})

Одна библиотека может сделать это легко: обещание-повтор.

Вот несколько примеров, чтобы проверить это:

const promiseRetry = require('promise-retry');

Ожидайте вторую попытку быть успешной:

it('should retry one time after error', (done) => {
    const options = {
        minTimeout: 10,
        maxTimeout: 100
    };
    promiseRetry((retry, number) => {
        console.log('test2 attempt number', number);
        return new Promise((resolve, reject) => {
            if (number === 1) throw new Error('first attempt fails');
            else resolve('second attempt success');
        }).catch(retry);
    }, options).then(res => {
        expect(res).toBe('second attempt success');
        done();
    }).catch(err => {
        fail(err);
    });
});

Ожидайте только одну попытку:

it('should not retry a second time', (done) => {
    const options = {
        retries: 1,
        minTimeout: 10,
        maxTimeout: 100
    };
    promiseRetry((retry, number) => {
        console.log('test4 attempt number', number);
        return new Promise((resolve, reject) => {
            if (number <= 2) throw new Error('attempt ' + number + ' fails');
            else resolve('third attempt success');
        }).catch(retry);
    }, options).then(res => {
        fail('Should never success');
    }).catch(err => {
        expect(err.toString()).toBe('Error: attempt 2 fails');
        done();
    });
});

Простая повторная попытка обещания:

      function keepTrying(otherArgs, promise) {
    promise = promise||new Promise();
    
    // try doing the important thing
    
    if(success) {
        promise.resolve(result);
    } else {
        setTimeout(function() {
            keepTrying(otherArgs, promise);
        }, retryInterval);
    }
}

Единственная простая в использовании и чистая Javascript с нулевой зависимостью , которая вам когда-либо понадобится.

Пример

      const { retry } = require('@ajimae/retry')

function exec() {
  // This will be any async or sync action that needs to be retried.
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ message: 'some async data' })
    }, 1500)
  })
}

// takes the response from the exec function and check if the condition/conditions are met
function predicate(response, retryCount) {
  console.log(retryCount) // goes from 0 to maxRetries 

  // once this condition is met the retry exits
    return (response.message == 'some async data')
}

(async function main() {
  // enable or disable an exponential backoff behaviour if needed.
  const result = await retry(exec, predicate, { maxRetries: 5, backoff: true })
  console.log(result) // { message: 'some async data' } 
})()

PS: Я является автором этой библиотека асинхронных повторовбиблиотеки .

Другие вопросы по тегам