承诺重试设计模式

Promise Retry Design Patterns

本文关键字:设计模式 重试 承诺      更新时间:2023-09-26

>Edit

  1. 不断重试直到承诺解决的模式(使用 delaymaxRetries (。
  2. 不断重试直到条件出现的模式根据结果满足(delaymaxRetries (。
  3. 具有无限次重试的内存高效动态模式(提供delay(。

#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);

有点不同...

异步重试可以通过构建.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. 不断重试直到条件满足结果的模式(具有延迟和最大重试次数(

这是以递归方式使用本机承诺执行此操作的好方法:

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

这就是你如何调用它,假设func有时成功,有时失败,总是返回一个我们可以记录的字符串:

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

在这里,我们调用 retryOperation,要求它每秒重试一次,最大重试次数 = 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声明的三个分支,而没有在其中创建新的承诺。 为了方便起见,我确实使用了一个基于 promise 的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);

在许多情况下可以使用的更可重用的重试方案将定义一些可重用的外部代码,但您似乎反对这样做,所以我没有制作该版本。


下面是另一种方法,它根据你的请求在Promise.prototype上使用.retryUntil()方法。 如果要调整此内容的实现细节,应该能够修改以下常规方法:

// 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);
<</div> div class="answers">

这是一个"指数退避"重试实现,使用可以包装任何承诺 API 的async/await

注意:出于演示原因,代码段使用 Math.random 模拟片状端点,因此请尝试几次以查看成功和失败案例。

/**
 * 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))

Check @jsier/retrier。经过测试,记录,轻量级,易于使用,没有外部依赖关系,并且已经在生产中已有一段时间了。

支持:

  • 首次尝试延迟
  • 尝试之间的延迟
  • 限制尝试次数
  • 回调以在满足某些条件时停止重试(例如,遇到特定错误(
  • 回调以在满足某些条件时继续重试(例如,解析的值不令人满意(

安装:

npm install @jsier/retrier

用法:

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!"
  );

包没有外部依赖项。

这是我

的尝试。我试图从上述所有答案中获取我喜欢的东西。没有外部依赖关系。打字稿 + 异步/等待 (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));

在 holmberd 的解决方案基础上构建

,代码更简洁,延迟
// Retry code
const wait = ms => new Promise((resolve) => {
  setTimeout(() => resolve(), ms)
})

const retryWithDelay = async (
  fn, retries = 3, interval = 50,
  finalErr = Error('Retry failed')
) => {
  try {
    await fn()
  } catch (err) {
    if (retries <= 0) {
      return Promise.reject(finalErr);
    }
    await wait(interval)
    return retryWithDelay(fn, (retries - 1), interval, finalErr);
  }
}
// Test
const getTestFunc = () => {
  let callCounter = 0
  return async () => {
    callCounter += 1
    if (callCounter < 5) {
      throw new Error('Not yet')
    }
  }
}
const test = async () => {
  await retryWithDelay(getTestFunc(), 10)
  console.log('success')
  await retryWithDelay(getTestFunc(), 3)
  console.log('will fail before getting here')  
}

test().catch(console.error)

如果你的代码被放在一个类中,你可以使用装饰器来实现这一点。你在实用程序装饰器(npm install --save utils-decorators(库中有这样的装饰器:

import {retry} from 'utils-decorators';
class SomeService {
   @retry(3)
   doSomeAsync(): Promise<any> {
    ....
   }
}

或者你可以使用包装器函数:

import {retryfy} from 'utils-decorators';
const withRetry = retryfy(originalFunc, 3);

注意:此库是树可摇动的,因此您不会为此库中的其余可用装饰器支付额外的字节。

https://github.com/vlio20/utils-decorators#retry-method

这里有很多答案,但经过一些研究,我决定采用递归方法。我将我的解决方案留给任何感兴趣的人

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);
      });
  });
}

这是一个带有漂亮游乐场的堆叠闪电战,您可以在那里感受它的工作原理。只需绕过 intent 变量即可查看承诺解析/拒绝

https://js-vjramh.stackblitz.io

不知道为什么提出的所有解决方案都是递归的。使用 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 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-retry.ts 正在尝试实现该模式,我正在生产中将其用于某些项目。

安装:

npm install 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

这是我的解决方案:

  • 使用打字稿保留函数类型。
  • 接受具有任何参数的函数。
  • 自定义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

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function retry(fn, maxRetries, getDelay = () => 5000) {
  let retries = 0;
  return async function wrapped(...args) {
    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();

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);

一个库可以很容易地做到这一点:promise-retry。

以下是一些测试它的示例:

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();
    });
});

我的 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 秒而不是毫秒,现在抛出导致失败的错误而不是硬编码的测量。

我给你一个异步/等待的解决方案,玩得开心:)

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();

以防万一有人正在寻找更通用的解决方案。这是我的两分钱:

辅助功能:

/**
 * 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 次,仅在 DOMError 秒内重试:

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

精细重试:(不要这样做!

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

简单承诺重试:

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);
    }
}

这对我来说非常有效:

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
            }
        }
    })
}

一种控制操作是否确实可以重试的方法

在实践中,我发现我们不应该假设操作总是可重试的,泛型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);
}

唯一易于使用的纯 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:我创作了这个库。