承诺重试设计模式

时间:2016-07-05 22:11:53

标签: javascript node.js promise

修改

  1. 继续重试的模式,直到诺言结算(延迟和延迟) maxRetries)。
  2. 继续重试直到条件的模式 满足结果(延迟和 maxRetries)。
  3. 具有无限重试(提供延迟)的内存高效动态模式。
  4. #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);
    

19 个答案:

答案 0 :(得分:36)

有点不同......

异步重试可以通过构建.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);

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

DEMO 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()可以是同步的,也可以是异步的。

添加进一步的测试也是微不足道的。只需在两个渔获物之间夹上一连串的thens。

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

DEMO https://jsfiddle.net/duL0qjqe/3/

所有版本都是为attempt设计的,是一个承诺返回异步函数。它也可以设想返回一个值,在这种情况下,链将跟随其成功路径到下一个/终端.then()

答案 1 :(得分:16)

<强> 2。继续重试的模式,直到条件满足结果(带延迟和maxRetries)

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

deny

这就是你如何调用它,假设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,要求它每秒重试一次,最大重试次数= 5.

如果你想要没有承诺的更简单的东西,RxJs会帮助你:https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/retrywhen.md

答案 2 :(得分:10)

您可以将新承诺链接到前一个承诺,从而延迟其最终解决方案,直到您知道最终答案。如果下一个答案仍然未知,那么将另一个答案链接到其上并继续将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()处理程序中,只需返回一个值即可解决,抛出一个异常就是拒绝,并且返回一个promise会将新的promise链接到前一个。这涵盖了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);

我不确定&#34;设计模式&#34;你正在寻找。由于您似乎反对外部声明的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);

答案 3 :(得分:9)

提到了很多好的解决方案,现在使用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);
    });
}

答案 4 :(得分:4)

如果将代码放置在类中,则可以使用装饰器。您在utils-decoratorsnpm install --save utils-decorators)库中有这样的装饰器:

import {retry} from 'utils-decorators';

class SomeService {

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

注意:该库可被树摇晃,因此您无需为该库中的其余可用装饰器支付额外的字节。

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

答案 5 :(得分:3)

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

该软件包没有外部依赖关系。

答案 6 :(得分:2)

这是我的尝试。我试图从以上所有答案中得出自己喜欢的东西。没有外部依赖性。打字稿+异步/等待(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));

答案 7 :(得分:0)

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

答案 8 :(得分:0)

简单承诺重试:

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

答案 9 :(得分:0)

不确定为什么提出的所有解决方案都是递归的。使用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');
}

答案 10 :(得分:0)

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

助手功能:

/**
 * 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 s上重试:

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

答案 11 :(得分:0)

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

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

答案 12 :(得分:0)

我对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秒(而不是ms),并且现在抛出导致失败的错误,而不是硬编码的消息。

答案 13 :(得分:0)

holmberd在解决方案的基础上,编写了一些更简洁的代码和一个延迟

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

}

答案 14 :(得分:0)

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

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

答案 15 :(得分:0)

async-retry.ts正在尝试实施该模式,我正在某些项目的生产中使用它。

安装:

npm install async-retry.ts --save

用法:

(?<![\w-])              # negative lookbehind, only match if no word character or hyphen is present
(
    (?:adm-){0,1}       # non-matching group containing adm- literally once or none, will be matched in the greater group
    username[0-9]{0,1}  # literally matching username and a digit, once or none
)
(?![\w-])               # negative lookahead, only match if no word character or hyphen is present

此软件包是一个非常新的软件包,但它源自长期存在的软件包co-retry,该软件包以生成器函数的方式实现了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)

答案 16 :(得分:0)

这是一个使用async / await的指数退避重试实现,可以包装任何promise 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))

答案 17 :(得分:0)

一个图书馆可以轻松完成此任务: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();
    });
});

答案 18 :(得分:0)

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