web-dev-qa-db-ja.com

約束の再試行設計パターン

編集

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

少し違う何か...

非同期の再試行は、通常の.catch()チェーンとは対照的に、.then()チェーンを構築することで実現できます。

このアプローチは次のとおりです。

  • 指定された最大試行回数でのみ可能です。 (チェーンは有限の長さでなければなりません)、
  • 低い最大値のみをお勧めします。 (Promiseチェーンは、その長さにほぼ比例してメモリを消費します)。

それ以外の場合は、再帰的なソリューションを使用してください。

最初に、.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);

DEMOhttps://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);

DEMOhttps://jsfiddle.net/duL0qjqe/1/

3。結果が何らかの条件を満たすまで、delayで再試行します

(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()は同期でも非同期でもかまいません。

さらにテストを追加するのも簡単です。 2つのキャッチの間にthensのチェーンを挟むだけです。

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

DEMOhttps://jsfiddle.net/duL0qjqe/3/


すべてのバージョンは、attemptが約束を返す非同期関数になるように設計されています。また、値を返すことも考えられます。その場合、チェーンはnext/terminal .then()への成功パスをたどります。

47
Roamer-1888

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を呼び出して、最大再試行= 5で毎秒再試行するように要求しています。

約束のないシンプルなものが必要な場合は、RxJsが役立ちます。 https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/retrywhen.md

19
Yair Kukielka

新しい約束を前の約束に連鎖させて、最終的な答えがわかるまで最終的な解決を遅らせることができます。次の答えがまだわからない場合は、別の約束を連鎖し、最終的に答えを知って最終的な解決策を返すことができるまで、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ステートメントの3つのブランチをカバーしますが、そこで新しい約束を作成することはありません。便宜上、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()メソッドを使用するもう1つのアプローチがあります。この実装の詳細を微調整する場合は、次の一般的なアプローチを変更できるはずです。

// 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);
10
jfriend00

言及された多くの良い解決策があり、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);
    });
}
9
holmberd

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をジェネレーター関数形式で実装しています。

1
Jeff Tian

1つのライブラリでこれを簡単に行うことができます: promise-retry

これをテストする例をいくつか示します。

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

2回目の試行が成功することを期待します。

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

再試行は1回のみです。

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

これは、promise APIをラップできるasync/awaitを使用した指数バックオフ再試行の実装です。数学ランダムで不安定なエンドポイントをシミュレートするので、成功と失敗の両方のケースを確認するために数回試してください。

/**
 * 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))
0
Red Mercury
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);
0
Hugo Silva