Trying to understand this retryWhen/scan/delay rxjs sequence - rxjs

So I came up with this implementation of a delayed retry for HTTP requests, this works fine and I have a good understanding of it as well.
retryWhen(err => {
let retryCount = 0;
let nextDelay = 0;
backoff = backoff < 0 || backoff === null ? DEFAULT_BACKOFF : backoff;
maxDelay = maxDelay < 0 || maxDelay === null ? DEFAULT_MAX_DELAY : maxDelay;
return err.pipe(
scan(idx => {
if (idx > maxRetries - 1) {
throw err;
} else {
idx++;
retryCount = idx;
nextDelay = Math.min(retryDelay + ((retryCount - 1) * backoff), maxDelay);
return idx;
}
}, 0),
tap(v => console.log(`Waiting ${nextDelay} ms for Retry #${retryCount}...`)),
delay(nextDelay),
tap(v => console.log(`Initiating HTTP Retry for context ${context}`))
)
First issue: The above code works fine when I use a constant value in the delay operator like so delay(3000) but it does not when I use the nextDelay variable. When I use the nextDelay variable there is no delay, it's like the variable is zero.
However the value is output correctly in the tap operator above it so I know it's in scope and the let is within scope of the retryWhen so should be good. I can't figure out why the delay does not work unless I use a value literal.
Second issue: I want to optimize the above code so that I don't use the variables retryCount and nextDelay, I want to compute those on the fly within the delay operator, however this operator takes only the amount of the delay as the input and does not have a reference to the idx emmitted by the scan above. I would like to do something like this:
scan(idx => ... code to either throw the error or emit the index ...),
delay(idx => Math.min(retryDelay + (idx - 1) * backoff), maxDelay)
The scan maps the original err element to the index idx but how exactly do I get that in the delay operator?

First issue: The above code works fine when I use a constant value in the delay operator like so delay(3000) but it does not when I use the nextDelay variable. When I use the nextDelay variable there is no delay, it's like the variable is zero.
The cause of the issue has been already pointed out in fridoo's answer.
However the value is output correctly in the tap operator
This is a hint of you could fix it. The difference is that delay(value) captures the value and in the case of tap(() => ...), the value will be evaluated every time its callback function will be invoked(in this case, on each Next notification).
If we take a look at delay's implementation
export function delay<T>(due: number | Date, scheduler: SchedulerLike = asyncScheduler): MonoTypeOperatorFunction<T> {
const duration = timer(due, scheduler);
return delayWhen(() => duration);
}
we'll see that it first captures the value in a timer observable, which will be then used with a delayWhen.
So, this first issue could be fixed with:
/* ... */
tap(v => console.log(`Waiting ${nextDelay} ms for Retry #${retryCount}...`)),
delayWhen(() => timer(nextDelay)),
tap(v => console.log(`Initiating HTTP Retry for context ${context}`))
/* ... */
Second issue: I want to optimize the above code so that I don't use the variables retryCount and nextDelay
We can use other RxJS operators for this:
retryWhen(err => {
backoff = backoff < 0 || backoff === null ? DEFAULT_BACKOFF : backoff;
maxDelay = maxDelay < 0 || maxDelay === null ? DEFAULT_MAX_DELAY : maxDelay;
return err.pipe(
// replacing `retryCount` with `map`'s index argument
map((err, idx) => {
// you can also throw the error here
if (idx >= maxRetries) { throw err; }
const retryCount = idx + 1;
// returning the `nextDelay`
return Math.min(retryDelay + ((retryCount - 1) * backoff), maxDelay);
})
delayWhen(nextDelay => timer(nextDelay)),
)

The function passed to retryWhen is only called once, on the first error. So delay(nextDelay) is called once when the err.pipe(...) observable is created (not when err emits). At that time nextDelay is still 0. You should put all your logic in an operator that returns a different value depending on values emitted by err.
To return an observable that emits with a changing delay you can mergeMap to a timer. This should get you started:
interface RetryStrategyConfig {
maxRetryAttempts?: number;
scalingDuration?: number;
maxDelay?: number;
excludedStatusCodes?: number[];
}
function genericRetryStrategy({
maxRetryAttempts = 6,
scalingDuration = 1000,
maxDelay = 5000,
excludedStatusCodes = []
}: RetryStrategyConfig = {}) {
return (attempts: Observable<any>) => {
return attempts.pipe(
mergeMap((error, i) => {
const retryAttempt = i + 1;
// if maximum number of retries have been met
// or response is a status code we don't wish to retry, throw error
if (
retryAttempt > maxRetryAttempts ||
excludedStatusCodes.find(e => e === error.status)
) {
return throwError(error);
}
const nextDelay = Math.min(retryAttempt * scalingDuration, maxDelay);
console.log(`Attempt ${retryAttempt}: retrying in ${nextDelay}ms`);
return timer(nextDelay);
}),
finalize(() => console.log("We are done!"))
);
};
}
obs$.pipe(
retryWhen(genericRetryStrategy())
)

Related

In the following code variable 'checkNumber' is not incrementing to 1 even after 'if' block get executed and so that Break is not working

In the following code variable 'checkNumber' is not incrementing to 1 even after 'if' block get executed and so that Break is not working where i need to break the loop
var checkNumber =0
for (let i = 0; i < totalRowCountAllocPrj; i++){
allocationObjects.getAllocationStatusfromGrid(i).then(text => {
appAllocStatus = Cypress.$(text).text()
cy.log("Allocation Status :" + appAllocStatus)
if(appAllocStatus == userData.approvalReservedAllocStatus){
allocationObjects.getAppAllCheckBoxesfromGrid(i).click()
checkNumber=checkNumber+1
cy.log("index="+i)
}
else{
cy.log("Project is not Reserved")
}
})
cy.log("number="+checkNumber)
if(checkNumber==1)
{
break
}
The variable checkNumber gets incremented inside an asynchronous command, but the loop is running synchronously.
You can't use break but you should be able to stop executing the commands with the inverse check.
Since checkNumber never should go above 0, it's more sensible to use a boolean
let found = 0
for (let i = 0; i < totalRowCountAllocPrj; i++) {
cy.then(() => {
if (!found) {
allocationObjects.getAllocationStatusfromGrid(i).then(text => {
appAllocStatus = Cypress.$(text).text()
if (appAllocStatus === userData.approvalReservedAllocStatus) {
allocationObjects.getAppAllCheckBoxesfromGrid(i).click()
found = true
}
})
}
})
}
BTW you should be able to rewrite allocationObjects.getAllocationStatusfromGrid() to directly search for userData.approvalReservedAllocStatus and get rid of the loop altogether.

How to cancel the execution or unsubscribe previous inner observable returned from switchmap

async function asyncFunction(source) {
console.log(source + ' started');
for (let i = 0; i < 4; ++i) {
console.log('"' + source + '"' + ' number ' + i);
await new Promise((r) => setTimeout(r, 1000));
}
console.log(source + ' completed');
return `asyncFuntion ${source} returns completed`;
}
const epic = interval(2000).pipe(switchMap(value => {
console.log("source value " + value);
return from(asyncFunction(value))
}));
epic.subscribe(
console.log,
console.error,
() => console.log('completed epic')
);
Above is my code and each time a new value gets emitted from the interval, I want the previous execution of asynFunction to stop running but switchMap does not do it. I am manually calling the subscribe method here, but in rxjs framework, I don't have to call the subscribe method since the framework is doing it for me somewhere. I have tried so many things (takeUtil, take and etc) and still unable to find the solution. All I want is for the previous execution/call to the asynFunction, which runs longer than the time it takes to get a new emitted value from the interval, to be terminated when a new source value is emitted.

rxjs custom retryWhen strategy with auto incremented delay not working properly

I'm trying to create a custom retryWhen strategy which attempts to retry N times with X delay in-between and fail afterwards. To some extent the learnrxjs.io example is exactly what I'm looking for.
Unfortunately there is an issue with this code which I can't seem to figure how to resolve.
In my case, the observable can fail randomly - you can have 2 successful attempts and then 2 unsuccessful attempts. After a while the subscription will automatically complete, because the retryAttempts will exceed the maximum although that has not happened in practice.
To better understand the issue I've created a StackBlitz
The response will be:
Attempt 1: retrying in 1000ms
0
1
Attempt 2: retrying in 2000ms
Attempt 3: retrying in 3000ms
0
1
We are done!
But it should actually be
Attempt 1: retrying in 1000ms
0
1
Attempt 1: retrying in 1000ms <-- notice counter starts from 1
Attempt 2: retrying in 2000ms
0
1
Attempt 1: retrying in 1000ms <-- notice counter starts from 1
0
1
Attempt 1: retrying in 1000ms <-- notice counter starts from 1
Attempt 2: retrying in 2000ms
0
1
... forever
I feel like I'm missing something here.
I think the example given in the docs is written for an Observable that only emits once and then completes, such as an http get. It is assumed that if you want to get more data then you will subscribe again which will reset the counter inside genericRetryStrategy. If, however, you now want to apply this same strategy to a long-running observable whose stream won't complete unless it gives an error (such as you have with interval()), then you'll need to modify genericRetryStrategy() to be told when the counter needs to be reset.
This could be done a number of ways, I have given a simple example in this StackBlitz based off of what you said you were trying to accomplish. Note that I also changed your logic slightly to more match what you said you were trying to do which is have '2 successful attempts and then 2 unsuccessful attempts'. The important bits though are modifying the error object that is thrown into genericRetryStrategy() to communicate the current count of failed attempts so it can react appropriately.
Here is the code copied here for completeness:
import { timer, interval, Observable, throwError } from 'rxjs';
import { map, switchMap, tap, retryWhen, delayWhen, mergeMap, shareReplay, finalize, catchError } from 'rxjs/operators';
console.clear();
interface Err {
status?: number;
msg?: string;
int: number;
}
export const genericRetryStrategy = ({
maxRetryAttempts = 3,
scalingDuration = 1000,
excludedStatusCodes = []
}: {
maxRetryAttempts?: number,
scalingDuration?: number,
excludedStatusCodes?: number[]
} = {}) => (attempts: Observable<any>) => {
return attempts.pipe(
mergeMap((error: Err) => {
// i here does not reset and continues to increment?
const retryAttempt = error.int;
// if maximum number of retries have been met
// or response is a status code we don't wish to retry, throw error
if (
retryAttempt > maxRetryAttempts ||
excludedStatusCodes.find(e => e === error.status)
) {
return throwError(error);
}
console.log(
`Attempt ${retryAttempt}: retrying in ${retryAttempt *
scalingDuration}ms`
);
// retry after 1s, 2s, etc...
return timer(retryAttempt * scalingDuration);
}),
finalize(() => console.log('We are done!'))
);
};
let int = 0;
let err: Err = {int: 0};
//emit value every 1s
interval(1000).pipe(
map((val) => {
if (val > 1) {
//error will be picked up by retryWhen
int++;
err.msg = "equals 1";
err.int = int;
throw err;
}
if (val === 0 && int === 1) {
err.msg = "greater than 2";
err.int = 2;
int=0;
throw err;
}
return val;
}),
retryWhen(genericRetryStrategy({
maxRetryAttempts: 3,
scalingDuration: 1000,
excludedStatusCodes: [],
}))
).subscribe(val => {
console.log(val)
});
To me this is still very imperative, but without understanding the problem you are trying to solve more deeply, I can't currently think of a more declarative approach...

RxJS: How to emit original values, then reduce upon completion?

I would like to emit all original values from an RxJS stream, and then emit a summary upon completion.
Reduce stops the original values from emitting. Scan emits each total rather than the original values.
Here is my hacky solution:
let total = {
total: 0
};
Rx.Observable.range(1, 3)
.do(val => {
total.total += val;
})
.concat(Rx.Observable.of(total))
.subscribe(
value => {
console.log('Next:', value)
}
);
// Next: 1
// Next: 2
// Next: 3
// Next: { total: 6 }
What is a simple way to do this with pure RxJS streams?
Use multicast
Rx.Observable.range(1, 3)
.multicast(new Rx.Subject(), (shared)=> {
return Rx.Observable.merge(shared, shared.reduce((acc, x)=>acc+x,0))
})
.subscribe(x=>console.log(x))
As an alternative, you could avoid using share() and making two Observable chains and make just a single chain:
Observable.range(1, 3)
.concat(Observable.of(null))
.scan((obj, curr) => {
if (curr) {
obj.acc.push(curr);
}
obj.curr = curr;
return obj;
}, { acc: [], curr: 0 })
.map(obj => obj.curr === null
? { total: (obj.acc.reduce((acc, curr) => acc + curr, 0)) } // count total
: obj.curr // just return current item
)
.subscribe(console.log);
This prints the result you're expecting:
1
2
3
{ total: 6 }
Even though using share() looks very simple be aware that it in fact you subscribe to the source Observable twice. In practise maybe it's not a problem for you depending on what source Observable you'll use.
Try this and see that each number is printed twice:
let source = Observable.range(1, 3).do(console.log).share();
How about?
let source = Observable.range(1, 3).share();
let totalOb = source
.reduce((total, value) => total + value, 0);
source
.concat(totalOb)
.subscribe( value => console.log(`Next: ${value}`) );
Output:
Next: 1
Next: 2
Next: 3
Next: 6
You can use throw and catch to separate data and summary.
let source = Observable.range(1, 3).share();
let totalOb = source
.reduce((total, value) => total + value, 0)
.mergeMap(total => Observable.throw(total));
source
.concat(totalOb)
.subscribe(
value => console.log(`Next: ${value}`),
value => console.log(`Total: ${value}`)
);
Output:
Next: 1
Next: 2
Next: 3
Total: 6

Linq to Objects - query objects for any non-numeric data

I am trying to write some logic to determine if all values of a certain property of an object in a collection are numeric and greater than zero. I can easily write this using ForEach but I'd like to do it using Linq to Object. I tried this:
var result = entity.Reports.Any(
x =>
x.QuestionBlock == _question.QuestionBlock
&& (!string.IsNullOrEmpty(x.Data)) && Int32.TryParse(x.Data, out tempVal)
&& Int32.Parse(x.Data) > 0);
It does not work correctly. I also tried this, hoping that the TryParse() on Int32 will return false the first time it encounter a string that cannot be parsed into an int. But it appears the out param will contain the first value string value that can be parsed into an int.
var result = entity.GranteeReportDataModels.Any(
x =>
x.QuestionBlock == _question.QuestionBlock
&& (!string.IsNullOrEmpty(x.Data)) && Int32.TryParse(x.Data, out tempVal));
Any help is greatly appreciated!
If you want to test if "all" values meet a condition, you should use the All extension method off IEnumerable<T>, not Any. I would write it like this:
var result = entity.Reports.All(x =>
{
int result = 0;
return int.TryParse(x.Data, out result) && result > 0;
});
I don't believe you need to test for an null or empty string, because int.TryPrase will return false if you pass in a null or empty string.
var allDataIsNatural = entity.Reports.All(r =>
{
int i;
if (!int.TryParse(r.Data, out i))
{
return false;
}
return i > 0;
});
Any will return when the first row is true but, you clearly say you would like to check them all.
You can use this extension which tries to parse a string to int and returns a int?:
public static int? TryGetInt(this string item)
{
int i;
bool success = int.TryParse(item, out i);
return success ? (int?)i : (int?)null;
}
Then this query works:
bool all = entity.Reports.All(x => {
if(x.QuestionBlock != _question.QuestionBlockint)
return false;
int? data = x.Data.TryGetInt();
return data.HasValue && data.Value > 0;
});
or more readable (a little bit less efficient):
bool all = entityReports
.All(x => x.Data.TryGetInt().HasValue && x.Data.TryGetInt() > 0
&& x.QuestionBlock == _question.QuestionBlockint);
This approach avoids using a local variable as out parameter which is an undocumented behaviour in Linq-To-Objects and might stop working in future. It's also more readable.

Resources