Шаблоны проектирования повторных попыток
Edit
- Шаблон, который продолжает повторять попытку до тех пор, пока не будет разрешено обещание (с задержкой и
maxRetries).
- Шаблон, который продолжает повторять попытку до состояния
отвечает на результат (с задержкой и
maxRetries).
- Эффективный динамический паттерн с неограниченными повторами (предусмотренная задержка).
Код для # 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);
Ответы
Ответ 1
Что-то немного другое...
.catch()
могут быть достигнуты путем создания .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
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
Ответ 3
Вы можете связать новое обещание с предыдущим, тем самым задерживая его возможное разрешение, пока не узнаете окончательный ответ. Если следующий ответ все еще неизвестен, тогда наложите на него другое обещание и продолжайте привязывать 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()
, которая основана на обещании.
FYI, это предполагает, что 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);
Ответ 4
Упоминается много хороших решений, и теперь с помощью 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);
});
}
Ответ 5
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);
Ответ 6
Одна библиотека может сделать это легко: обещание-повтор.
Вот несколько примеров, чтобы проверить это:
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();
});
});