Skip to content

Commit 88124a7

Browse files
feat(effects): limit retries to 10 by default (#2376)
Closes #2303
1 parent c305086 commit 88124a7

File tree

4 files changed

+40
-16
lines changed

4 files changed

+40
-16
lines changed

modules/effects/spec/effects_error_handler.spec.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
import { ErrorHandler, Provider } from '@angular/core';
1+
import { ErrorHandler, Provider, Type } from '@angular/core';
22
import { TestBed } from '@angular/core/testing';
33
import { Action, Store } from '@ngrx/store';
4-
import { Observable, of } from 'rxjs';
4+
import { Observable, of, throwError } from 'rxjs';
55
import { catchError } from 'rxjs/operators';
66
import { createEffect, EFFECTS_ERROR_HANDLER, EffectsModule } from '..';
7+
import * as effectsSrc from '../src/effects_error_handler';
78

89
describe('Effects Error Handler', () => {
910
let subscriptionCount: number;
1011
let globalErrorHandler: jasmine.Spy;
1112
let storeNext: jasmine.Spy;
1213

13-
function makeEffectTestBed(...providers: Provider[]) {
14+
function makeEffectTestBed(effect: Type<any>, ...providers: Provider[]) {
1415
subscriptionCount = 0;
1516

1617
TestBed.configureTestingModule({
17-
imports: [EffectsModule.forRoot([ErrorEffect])],
18+
imports: [EffectsModule.forRoot([effect])],
1819
providers: [
1920
{
2021
provide: Store,
@@ -38,8 +39,14 @@ describe('Effects Error Handler', () => {
3839
storeNext = store.next;
3940
}
4041

42+
it('should retry on infinite error up to 10 times', () => {
43+
makeEffectTestBed(AlwaysErrorEffect);
44+
45+
expect(globalErrorHandler.calls.count()).toBe(10);
46+
});
47+
4148
it('should retry and notify error handler when effect error handler is not provided', () => {
42-
makeEffectTestBed();
49+
makeEffectTestBed(ErrorEffect);
4350

4451
// two subscriptions expected:
4552
// 1. Initial subscription to the effect (this will error)
@@ -62,7 +69,7 @@ describe('Effects Error Handler', () => {
6269
);
6370
});
6471

65-
makeEffectTestBed({
72+
makeEffectTestBed(ErrorEffect, {
6673
provide: EFFECTS_ERROR_HANDLER,
6774
useValue: effectsErrorHandlerSpy,
6875
});
@@ -84,6 +91,10 @@ describe('Effects Error Handler', () => {
8491
});
8592
}
8693

94+
class AlwaysErrorEffect {
95+
effect$ = createEffect(() => throwError('always an error'));
96+
}
97+
8798
/**
8899
* This observable factory returns an observable that will never emit, but the first subscriber will get an immediate
89100
* error. All subsequent subscribers will just get an observable that does not emit.

modules/effects/src/effects_error_handler.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,25 @@ export type EffectsErrorHandler = <T extends Action>(
88
errorHandler: ErrorHandler
99
) => Observable<T>;
1010

11-
export const defaultEffectsErrorHandler: EffectsErrorHandler = <
12-
T extends Action
13-
>(
11+
const MAX_NUMBER_OF_RETRY_ATTEMPTS = 10;
12+
13+
export function defaultEffectsErrorHandler<T extends Action>(
1414
observable$: Observable<T>,
15-
errorHandler: ErrorHandler
16-
): Observable<T> => {
15+
errorHandler: ErrorHandler,
16+
retryAttemptLeft: number = MAX_NUMBER_OF_RETRY_ATTEMPTS
17+
): Observable<T> {
1718
return observable$.pipe(
1819
catchError(error => {
1920
if (errorHandler) errorHandler.handleError(error);
21+
if (retryAttemptLeft <= 1) {
22+
return observable$; // last attempt
23+
}
2024
// Return observable that produces this particular effect
21-
return defaultEffectsErrorHandler(observable$, errorHandler);
25+
return defaultEffectsErrorHandler(
26+
observable$,
27+
errorHandler,
28+
retryAttemptLeft - 1
29+
);
2230
})
2331
);
24-
};
32+
}

modules/effects/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ export { EffectConfig } from './models';
33
export { Effect } from './effect_decorator';
44
export { getEffectsMetadata } from './effects_metadata';
55
export { mergeEffects } from './effects_resolver';
6-
export { EffectsErrorHandler } from './effects_error_handler';
6+
export {
7+
EffectsErrorHandler,
8+
defaultEffectsErrorHandler,
9+
} from './effects_error_handler';
710
export { EffectsMetadata, CreateEffectMetadata } from './models';
811
export { Actions, ofType } from './actions';
912
export { EffectsModule } from './effects_module';

projects/ngrx.io/content/guide/effects/lifecycle.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export class LogEffects {
4343
Starting with version 8, when an error happens in the effect's main stream it is
4444
reported using Angular's `ErrorHandler`, and the source effect is
4545
**automatically** resubscribed to (instead of completing), so it continues to
46-
listen to all dispatched Actions.
46+
listen to all dispatched Actions. By default, effects are resubscribed up to 10
47+
errors.
4748

4849
Generally, errors should be handled by users. However, for the cases where errors were missed,
4950
this new behavior adds an additional safety net.
@@ -96,7 +97,8 @@ The behavior of the default resubscription handler can be customized
9697
by providing a custom handler using the `EFFECTS_ERROR_HANDLER` injection token.
9798

9899
This allows you to provide a custom behavior, such as only retrying on
99-
certain "retryable" errors, or with maximum number of retries.
100+
certain "retryable" errors, or change the maximum number of retries (it's set to
101+
10 by default).
100102

101103
<code-example header="customise-error-handler.effects.ts">
102104
import { ErrorHandler, NgModule } from '@angular/core';

0 commit comments

Comments
 (0)