Angular 7 docs提供了rxjs
Observable
在为AJAX请求实现指数补偿时的实际用法示例:
import { pipe, range, timer, zip } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { retryWhen, map, mergeMap } from 'rxjs/operators';
function backoff(maxTries, ms) {
return pipe(
retryWhen(attempts => range(1, maxTries)
.pipe(
zip(attempts, (i) => i),
map(i => i * i),
mergeMap(i => timer(i * ms))
)
)
);
}
ajax('/api/endpoint')
.pipe(backoff(3, 250))
.subscribe(data => handleData(data));
function handleData(data) {
// ...
}
虽然我了解Observable和退避的概念,但我还不太清楚retryWhen
将如何精确地计算出重新订阅源ajax
的时间间隔。
具体地说,zip
,map
和mapMerge
在此设置中如何工作?
attempts
对象被发射到retryWhen
中时会包含什么?
我浏览了他们的参考页,但仍然无法解决这个问题。
答案 0 :(得分:6)
(出于学习目的)我花了很多时间对此进行研究,并将尝试尽可能彻底地解释此代码的工作原理。
首先,这是原始代码,带注释:
import { pipe, range, timer, zip } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { retryWhen, map, mergeMap } from 'rxjs/operators';
function backoff(maxTries, ms) { // (1)
return pipe( // (2)
retryWhen(attempts => range(1, maxTries) // (3)
.pipe(
zip(attempts, (i) => i), // (4)
map(i => i * i), // (5)
mergeMap(i => timer(i * ms)) // (6)
)
)
); // (7)
}
ajax('/api/endpoint')
.pipe(backoff(3, 250))
.subscribe(data => handleData(data));
function handleData(data) {
// ...
}
backoff
运算符中创建了自定义retryWhen
运算符。我们稍后将可以在pipe
函数中应用它。pipe
方法返回一个自定义运算符。我们的自定义运算符将是经过修改的retryWhen
运算符。它带有一个函数参数。此函数将被称为一次 —特别是在首次遇到/调用此retryWhen
时。顺便说一句,当可观察到的源产生错误时,retryWhen
仅在 中起作用。然后,它可以防止错误进一步传播并重新订阅源。如果源产生非错误结果(无论是第一次订阅还是重试),则retryWhen
会被传递并且不涉及。
attempts
上的几句话。这是可以观察的。这不是可观察到的源。它是专门为retryWhen
创建的。它只有一种用途:只有可观察到的对源可观察的订阅(或重新订阅)导致错误时,attempts
会触发next
。我们被赋予attempts
并可以自由使用它,以便以某种方式对可观察到的可观察源的每次失败订阅做出反应。
这就是我们要做的。
首先,我们创建range(1, maxTries)
,这是一个可观察值,对于我们愿意执行的每次重试,它都有一个整数。 range
已准备好在此时和那里解雇所有号码,但我们必须坚守一道:只有在发生其他重试时,我们才需要一个新号码。因此,这就是我们...
...用attempts
压缩。意思是,将每个attempts
的发射值与单个值range
结婚。
请记住,我们当前正在使用的函数将仅被调用一次,届时,attempts
将仅触发next
一次,用于初始失败的订阅。因此,在这一点上,我们的两个压缩观测值只产生了一个值。
顺便说一句,将两个可观察值压缩成一个的值是多少?该函数确定:(i) => i
。为了清楚起见,可以将其写为(itemFromRange, itemFromAttempts) => itemFromRange
。第二个参数未使用,因此被删除,第一个参数重命名为i
。
这里发生的是,我们只是无视attempts
触发的值,我们只对触发它们的事实感兴趣。每当发生这种情况时,我们都会从range
中观察到下一个值...
...并将其平方。这是用于指数补偿的指数部分。
因此,现在无论何时(重新)订阅源失败,我们手上的整数都会不断增加(1、4、9、16 ...)。我们如何将整数转换为时间延迟直到下一次重新订阅?
请记住,我们当前使用的此函数必须使用attempts
作为输入返回一个可观察的对象。此结果可观察到的对象仅构建一次。 retryWhen
然后订阅产生的可观察结果,并且:每当产生可观察到的火灾next
时,重试订阅可观察的源;每当可观察到的结果触发那些相应事件时,就在可观察到的源上调用complete
或error
。
长话短说,我们需要让retryWhen
稍等一下。可以使用delay
运算符,但是设置延迟的指数增长可能会很痛苦。取而代之的是mergeMap
运算符。
mergeMap
是两个运算符组合的快捷方式:map
和mergeAll
。 map
只需将每个递增的整数(1、4、9、16 ...)转换为timer
可观察到的值,该值在经过毫秒数后将触发next
。 mergeAll
强制retryWhen
实际订阅timer
。如果最后一点没有发生,我们得到的可观察对象将立即使用next
可观察实例作为值来触发timer
。
目前,我们已经构建了自定义可观察对象,retryWhen
将使用它来决定何时确切尝试重新订阅源可观察对象。
就目前而言,我发现此实现存在两个问题:
一旦我们产生的可观察结果触发了它的最后一个next
(导致了最后一次尝试重新订阅),它也会立即触发complete
。除非源可观察到的返回结果很快非常(假设最后一次重试将是成功的),否则该结果将被忽略。
这是因为一旦retryWhen
从我们的观察对象中听到complete
,它就会在源上调用complete
,这可能仍在发出AJAX请求中。
如果所有重试均未成功,则源实际上会调用complete
而不是更具逻辑性的error
。
要解决这两个问题,我认为在给最后一次重试提供一定的时间来尝试完成其工作后,最终得到的可观察值应在最后触发error
。
这是我对上述修复程序的实施,其中还考虑了zip
在最近的rxjs v6
中对运算符的弃用:
import { delay, dematerialize, map, materialize, retryWhen, switchMap } from "rxjs/operators";
import { concat, pipe, range, throwError, timer, zip } from "rxjs";
function backoffImproved(maxTries, ms) {
return pipe(
retryWhen(attempts => {
const observableForRetries =
zip(range(1, maxTries), attempts)
.pipe(
map(([elemFromRange, elemFromAttempts]) => elemFromRange),
map(i => i * i),
switchMap(i => timer(i * ms))
);
const observableForFailure =
throwError(new Error('Could not complete AJAX request'))
.pipe(
materialize(),
delay(1000),
dematerialize()
);
return concat(observableForRetries, observableForFailure);
})
);
}
我测试了此代码,它似乎在所有情况下都能正常工作。我现在不愿意详细解释它;我怀疑有人还会读上面的文字墙。
无论如何,非常感谢@BenjaminGruenbaum和@cartant使我走上正确的道路,将我的头缠在这一切上。
答案 1 :(得分:0)
这是一个更简单的版本,可以轻松扩展/修改:
import { Observable, pipe, throwError, timer } from 'rxjs';
import { mergeMap, retryWhen } from 'rxjs/operators';
export function backoff(maxRetries = 5): (_: Observable<any>) => Observable<any> {
return pipe(
retryWhen(errors => errors.pipe(
mergeMap((error, i) => {
const retryAttempt = i + 1;
if (retryAttempt > maxRetries) {
return throwError(error);
} else {
const waitms = retryAttempt * retryAttempt * 1000;
console.log(`Attempt ${retryAttempt}: retrying in ${waitms}ms`);
return timer(waitms);
}
}),
))
);
};
参考 retryWhen