Retry on error if there's lower priority pending work (#12957)
* Remove enableSuspense flag from PendingPriority module We're going to use this for suspending on error, too. * Retry on error if there's lower priority pending work If an error is thrown, and there's lower priority work, it's possible the lower priority work will fix the error. Retry at the lower priority. If an error is thrown and there's no more work to try, handle the error like we normally do (trigger the nearest error boundary).
Andrew Clark committed
Jun 13, 2018 at 10:47 UTC
e0c78344e2f49fb91a75607ccdb170b5649bb1e9
4 files changed
+190
-157
packages/react-reconciler/src/ReactFiberPendingPriority.js
+117
-131
@@ -12,30 +12,26 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
12
13
import {NoWork} from './ReactFiberExpirationTime';
14
15
-import {enableSuspense} from 'shared/ReactFeatureFlags';
16
-
15
// TODO: Offscreen updates
16
17
export function markPendingPriorityLevel(
18
root: FiberRoot,
19
expirationTime: ExpirationTime,
20
): void {
23
- if (enableSuspense) {
24
- // Update the latest and earliest pending times
25
- const earliestPendingTime = root.earliestPendingTime;
26
- if (earliestPendingTime === NoWork) {
27
- // No other pending updates.
28
- root.earliestPendingTime = root.latestPendingTime = expirationTime;
21
+ // Update the latest and earliest pending times
22
+ const earliestPendingTime = root.earliestPendingTime;
23
+ if (earliestPendingTime === NoWork) {
24
+ // No other pending updates.
25
+ root.earliestPendingTime = root.latestPendingTime = expirationTime;
26
+ } else {
27
+ if (earliestPendingTime > expirationTime) {
28
+ // This is the earliest pending update.
29
+ root.earliestPendingTime = expirationTime;
30
} else {
30
- if (earliestPendingTime > expirationTime) {
31
- // This is the earliest pending update.
32
- root.earliestPendingTime = expirationTime;
33
- } else {
34
- const latestPendingTime = root.latestPendingTime;
35
- if (latestPendingTime < expirationTime) {
36
- // This is the latest pending update
37
- root.latestPendingTime = expirationTime;
38
- }
31
+ const latestPendingTime = root.latestPendingTime;
32
+ if (latestPendingTime < expirationTime) {
33
+ // This is the latest pending update
34
+ root.latestPendingTime = expirationTime;
35
}
36
}
37
}
@@ -46,114 +42,110 @@ export function markCommittedPriorityLevels(
42
currentTime: ExpirationTime,
43
earliestRemainingTime: ExpirationTime,
44
): void {
49
- if (enableSuspense) {
50
- if (earliestRemainingTime === NoWork) {
51
- // Fast path. There's no remaining work. Clear everything.
52
- root.earliestPendingTime = NoWork;
53
- root.latestPendingTime = NoWork;
54
- root.earliestSuspendedTime = NoWork;
55
- root.latestSuspendedTime = NoWork;
56
- root.latestPingedTime = NoWork;
57
- return;
58
- }
45
+ if (earliestRemainingTime === NoWork) {
46
+ // Fast path. There's no remaining work. Clear everything.
47
+ root.earliestPendingTime = NoWork;
48
+ root.latestPendingTime = NoWork;
49
+ root.earliestSuspendedTime = NoWork;
50
+ root.latestSuspendedTime = NoWork;
51
+ root.latestPingedTime = NoWork;
52
+ return;
53
+ }
54
60
- // Let's see if the previous latest known pending level was just flushed.
61
- const latestPendingTime = root.latestPendingTime;
62
- if (latestPendingTime !== NoWork) {
63
- if (latestPendingTime < earliestRemainingTime) {
64
- // We've flushed all the known pending levels.
65
- root.earliestPendingTime = root.latestPendingTime = NoWork;
66
- } else {
67
- const earliestPendingTime = root.earliestPendingTime;
68
- if (earliestPendingTime < earliestRemainingTime) {
69
- // We've flushed the earliest known pending level. Set this to the
70
- // latest pending time.
71
- root.earliestPendingTime = root.latestPendingTime;
72
- }
55
+ // Let's see if the previous latest known pending level was just flushed.
56
+ const latestPendingTime = root.latestPendingTime;
57
+ if (latestPendingTime !== NoWork) {
58
+ if (latestPendingTime < earliestRemainingTime) {
59
+ // We've flushed all the known pending levels.
60
+ root.earliestPendingTime = root.latestPendingTime = NoWork;
61
+ } else {
62
+ const earliestPendingTime = root.earliestPendingTime;
63
+ if (earliestPendingTime < earliestRemainingTime) {
64
+ // We've flushed the earliest known pending level. Set this to the
65
+ // latest pending time.
66
+ root.earliestPendingTime = root.latestPendingTime;
67
}
68
}
69
+ }
70
76
- // Now let's handle the earliest remaining level in the whole tree. We need to
77
- // decide whether to treat it as a pending level or as suspended. Check
78
- // it falls within the range of known suspended levels.
79
-
80
- const earliestSuspendedTime = root.earliestSuspendedTime;
81
- if (earliestSuspendedTime === NoWork) {
82
- // There's no suspended work. Treat the earliest remaining level as a
83
- // pending level.
84
- markPendingPriorityLevel(root, earliestRemainingTime);
85
- return;
86
- }
71
+ // Now let's handle the earliest remaining level in the whole tree. We need to
72
+ // decide whether to treat it as a pending level or as suspended. Check
73
+ // it falls within the range of known suspended levels.
74
88
- const latestSuspendedTime = root.latestSuspendedTime;
89
- if (earliestRemainingTime > latestSuspendedTime) {
90
- // The earliest remaining level is later than all the suspended work. That
91
- // means we've flushed all the suspended work.
92
- root.earliestSuspendedTime = NoWork;
93
- root.latestSuspendedTime = NoWork;
94
- root.latestPingedTime = NoWork;
95
-
96
- // There's no suspended work. Treat the earliest remaining level as a
97
- // pending level.
98
- markPendingPriorityLevel(root, earliestRemainingTime);
99
- return;
100
- }
75
+ const earliestSuspendedTime = root.earliestSuspendedTime;
76
+ if (earliestSuspendedTime === NoWork) {
77
+ // There's no suspended work. Treat the earliest remaining level as a
78
+ // pending level.
79
+ markPendingPriorityLevel(root, earliestRemainingTime);
80
+ return;
81
+ }
82
102
- if (earliestRemainingTime < earliestSuspendedTime) {
103
- // The earliest remaining time is earlier than all the suspended work.
104
- // Treat it as a pending update.
105
- markPendingPriorityLevel(root, earliestRemainingTime);
106
- return;
107
- }
83
+ const latestSuspendedTime = root.latestSuspendedTime;
84
+ if (earliestRemainingTime > latestSuspendedTime) {
85
+ // The earliest remaining level is later than all the suspended work. That
86
+ // means we've flushed all the suspended work.
87
+ root.earliestSuspendedTime = NoWork;
88
+ root.latestSuspendedTime = NoWork;
89
+ root.latestPingedTime = NoWork;
90
+
91
+ // There's no suspended work. Treat the earliest remaining level as a
92
+ // pending level.
93
+ markPendingPriorityLevel(root, earliestRemainingTime);
94
+ return;
95
+ }
96
109
- // The earliest remaining time falls within the range of known suspended
110
- // levels. We should treat this as suspended work.
97
+ if (earliestRemainingTime < earliestSuspendedTime) {
98
+ // The earliest remaining time is earlier than all the suspended work.
99
+ // Treat it as a pending update.
100
+ markPendingPriorityLevel(root, earliestRemainingTime);
101
+ return;
102
}
103
+
104
+ // The earliest remaining time falls within the range of known suspended
105
+ // levels. We should treat this as suspended work.
106
}
107
108
export function markSuspendedPriorityLevel(
109
root: FiberRoot,
110
suspendedTime: ExpirationTime,
111
): void {
118
- if (enableSuspense) {
119
- // First, check the known pending levels and update them if needed.
120
- const earliestPendingTime = root.earliestPendingTime;
121
- const latestPendingTime = root.latestPendingTime;
122
- if (earliestPendingTime === suspendedTime) {
123
- if (latestPendingTime === suspendedTime) {
124
- // Both known pending levels were suspended. Clear them.
125
- root.earliestPendingTime = root.latestPendingTime = NoWork;
126
- } else {
127
- // The earliest pending level was suspended. Clear by setting it to the
128
- // latest pending level.
129
- root.earliestPendingTime = latestPendingTime;
130
- }
131
- } else if (latestPendingTime === suspendedTime) {
132
- // The latest pending level was suspended. Clear by setting it to the
112
+ // First, check the known pending levels and update them if needed.
113
+ const earliestPendingTime = root.earliestPendingTime;
114
+ const latestPendingTime = root.latestPendingTime;
115
+ if (earliestPendingTime === suspendedTime) {
116
+ if (latestPendingTime === suspendedTime) {
117
+ // Both known pending levels were suspended. Clear them.
118
+ root.earliestPendingTime = root.latestPendingTime = NoWork;
119
+ } else {
120
+ // The earliest pending level was suspended. Clear by setting it to the
121
// latest pending level.
134
- root.latestPendingTime = earliestPendingTime;
122
+ root.earliestPendingTime = latestPendingTime;
123
}
124
+ } else if (latestPendingTime === suspendedTime) {
125
+ // The latest pending level was suspended. Clear by setting it to the
126
+ // latest pending level.
127
+ root.latestPendingTime = earliestPendingTime;
128
+ }
129
137
- // Next, if we're working on the lowest known suspended level, clear the ping.
138
- // TODO: What if a promise suspends and pings before the root completes?
139
- const latestSuspendedTime = root.latestSuspendedTime;
140
- if (latestSuspendedTime === suspendedTime) {
141
- root.latestPingedTime = NoWork;
142
- }
130
+ // Next, if we're working on the lowest known suspended level, clear the ping.
131
+ // TODO: What if a promise suspends and pings before the root completes?
132
+ const latestSuspendedTime = root.latestSuspendedTime;
133
+ if (latestSuspendedTime === suspendedTime) {
134
+ root.latestPingedTime = NoWork;
135
+ }
136
144
- // Finally, update the known suspended levels.
145
- const earliestSuspendedTime = root.earliestSuspendedTime;
146
- if (earliestSuspendedTime === NoWork) {
147
- // No other suspended levels.
148
- root.earliestSuspendedTime = root.latestSuspendedTime = suspendedTime;
149
- } else {
150
- if (earliestSuspendedTime > suspendedTime) {
151
- // This is the earliest suspended level.
152
- root.earliestSuspendedTime = suspendedTime;
153
- } else if (latestSuspendedTime < suspendedTime) {
154
- // This is the latest suspended level
155
- root.latestSuspendedTime = suspendedTime;
156
- }
137
+ // Finally, update the known suspended levels.
138
+ const earliestSuspendedTime = root.earliestSuspendedTime;
139
+ if (earliestSuspendedTime === NoWork) {
140
+ // No other suspended levels.
141
+ root.earliestSuspendedTime = root.latestSuspendedTime = suspendedTime;
142
+ } else {
143
+ if (earliestSuspendedTime > suspendedTime) {
144
+ // This is the earliest suspended level.
145
+ root.earliestSuspendedTime = suspendedTime;
146
+ } else if (latestSuspendedTime < suspendedTime) {
147
+ // This is the latest suspended level
148
+ root.latestSuspendedTime = suspendedTime;
149
}
150
}
151
}
@@ -162,35 +154,29 @@ export function markPingedPriorityLevel(
154
root: FiberRoot,
155
pingedTime: ExpirationTime,
156
): void {
165
- if (enableSuspense) {
166
- const latestSuspendedTime = root.latestSuspendedTime;
167
- if (latestSuspendedTime !== NoWork && latestSuspendedTime <= pingedTime) {
168
- const latestPingedTime = root.latestPingedTime;
169
- if (latestPingedTime === NoWork || latestPingedTime < pingedTime) {
170
- root.latestPingedTime = pingedTime;
171
- }
157
+ const latestSuspendedTime = root.latestSuspendedTime;
158
+ if (latestSuspendedTime !== NoWork && latestSuspendedTime <= pingedTime) {
159
+ const latestPingedTime = root.latestPingedTime;
160
+ if (latestPingedTime === NoWork || latestPingedTime < pingedTime) {
161
+ root.latestPingedTime = pingedTime;
162
}
163
}
164
}
165
166
export function findNextPendingPriorityLevel(root: FiberRoot): ExpirationTime {
177
- if (enableSuspense) {
178
- const earliestSuspendedTime = root.earliestSuspendedTime;
179
- const earliestPendingTime = root.earliestPendingTime;
180
- if (earliestSuspendedTime === NoWork) {
181
- // Fast path. There's no suspended work.
182
- return earliestPendingTime;
183
- }
184
-
185
- // First, check if there's known pending work.
186
- if (earliestPendingTime !== NoWork) {
187
- return earliestPendingTime;
188
- }
167
+ const earliestSuspendedTime = root.earliestSuspendedTime;
168
+ const earliestPendingTime = root.earliestPendingTime;
169
+ if (earliestSuspendedTime === NoWork) {
170
+ // Fast path. There's no suspended work.
171
+ return earliestPendingTime;
172
+ }
173
190
- // Finally, if a suspended level was pinged, work on that. Otherwise there's
191
- // nothing to work on.
192
- return root.latestPingedTime;
193
- } else {
194
- return root.current.expirationTime;
174
+ // First, check if there's known pending work.
175
+ if (earliestPendingTime !== NoWork) {
176
+ return earliestPendingTime;
177
}
178
+
179
+ // Finally, if a suspended level was pinged, work on that. Otherwise there's
180
+ // nothing to work on.
181
+ return root.latestPingedTime;
182
}
packages/react-reconciler/src/ReactFiberScheduler.js
+26
-24
@@ -237,8 +237,6 @@ let nextEffect: Fiber | null = null;
237
238
let isCommitting: boolean = false;
239
240
-let isRootReadyForCommit: boolean = false;
241
-
240
let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
241
242
// Used for performance tracking.
@@ -349,8 +347,6 @@ function resetStack() {
347
nextLatestTimeoutMs = -1;
348
nextRenderIsExpired = false;
349
nextUnitOfWork = null;
352
-
353
- isRootReadyForCommit = false;
350
}
351
352
function commitAllHostEffects() {
@@ -518,7 +514,12 @@ function commitRoot(finishedWork: Fiber): ExpirationTime {
514
);
515
root.pendingCommitExpirationTime = NoWork;
516
517
+ // Update the pending priority levels to account for the work that we are
518
+ // about to commit. This needs to happen before calling the lifecycles, since
519
+ // they may schedule additional updates.
520
+ const earliestRemainingTime = finishedWork.expirationTime;
521
const currentTime = recalculateCurrentTime();
522
+ markCommittedPriorityLevels(root, currentTime, earliestRemainingTime);
523
524
// Reset this to null before calling lifecycles
525
ReactCurrentOwner.current = null;
@@ -689,14 +690,13 @@ function commitRoot(finishedWork: Fiber): ExpirationTime {
690
ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork);
691
}
692
692
- markCommittedPriorityLevels(root, currentTime, root.current.expirationTime);
693
- const remainingTime = findNextPendingPriorityLevel(root);
694
- if (remainingTime === NoWork) {
693
+ const nextPendingPriorityLevel = findNextPendingPriorityLevel(root);
694
+ if (nextPendingPriorityLevel === NoWork) {
695
// If there's no remaining work, we can clear the set of already failed
696
// error boundaries.
697
legacyErrorBoundariesThatAlreadyFailed = null;
698
}
699
- return remainingTime;
699
+ return nextPendingPriorityLevel;
700
}
701
702
function resetExpirationTime(
@@ -847,7 +847,6 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
847
continue;
848
} else {
849
// We've reached the root.
850
- isRootReadyForCommit = true;
850
return null;
851
}
852
} else {
@@ -1097,11 +1096,11 @@ function renderRoot(
1096
} while (true);
1097
1098
// We're done performing work. Time to clean up.
1100
- let didCompleteRoot = false;
1099
isWorking = false;
1100
1101
// Yield back to main thread.
1102
if (didFatal) {
1103
+ const didCompleteRoot = false;
1104
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1105
interruptedBy = null;
1106
// There was a fatal error.
@@ -1111,34 +1110,38 @@ function renderRoot(
1110
return null;
1111
} else if (nextUnitOfWork === null) {
1112
// We reached the root.
1114
- if (isRootReadyForCommit) {
1115
- didCompleteRoot = true;
1113
+ const rootWorkInProgress = root.current.alternate;
1114
+ invariant(
1115
+ rootWorkInProgress !== null,
1116
+ 'Finished root should have a work-in-progress. This error is likely ' +
1117
+ 'caused by a bug in React. Please file an issue.',
1118
+ );
1119
+ if ((rootWorkInProgress.effectTag & Incomplete) === NoEffect) {
1120
+ const didCompleteRoot = true;
1121
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1122
interruptedBy = null;
1118
- // The root successfully completed. It's ready for commit.
1123
+ // The root successfully completed.
1124
root.pendingCommitExpirationTime = expirationTime;
1120
- const finishedWork = root.current.alternate;
1121
- return finishedWork;
1125
+ // Return the completed work-in-progress.
1126
+ return rootWorkInProgress;
1127
} else {
1128
// The root did not complete.
1129
+ const didCompleteRoot = false;
1130
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1131
interruptedBy = null;
1126
- invariant(
1127
- !nextRenderIsExpired,
1128
- 'Expired work should have completed. This error is likely caused ' +
1129
- 'by a bug in React. Please file an issue.',
1130
- );
1131
- markSuspendedPriorityLevel(root, expirationTime);
1132
if (nextLatestTimeoutMs >= 0) {
1133
setTimeout(() => {
1134
retrySuspendedRoot(root, expirationTime);
1135
}, nextLatestTimeoutMs);
1136
}
1137
+ markSuspendedPriorityLevel(root, expirationTime);
1138
const firstUnblockedExpirationTime = findNextPendingPriorityLevel(root);
1139
onBlock(firstUnblockedExpirationTime);
1140
+ // Return null to indicate that the root did not complete.
1141
return null;
1142
}
1143
} else {
1144
+ const didCompleteRoot = false;
1145
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1146
interruptedBy = null;
1147
// There's more work to do, but we ran out of time. Yield back to
@@ -1294,8 +1297,7 @@ function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
1297
return expirationTime;
1298
}
1299
1297
-// TODO: Rename this to scheduleTimeout or something
1298
-function suspendRoot(
1300
+function markTimeout(
1301
root: FiberRoot,
1302
thenable: Thenable,
1303
timeoutMs: number,
@@ -1978,7 +1980,7 @@ export {
1980
computeExpirationForFiber,
1981
captureCommitPhaseError,
1982
onUncaughtError,
1981
- suspendRoot,
1983
+ markTimeout,
1984
retrySuspendedRoot,
1985
markLegacyErrorBoundaryAsFailed,
1986
isAlreadyFailedLegacyErrorBoundary,
packages/react-reconciler/src/ReactFiberUnwindWork.js
+16
-2
@@ -55,7 +55,7 @@ import {
55
recordElapsedActualRenderTime,
56
} from './ReactProfilerTimer';
57
import {
58
- suspendRoot,
58
+ markTimeout,
59
onUncaughtError,
60
markLegacyErrorBoundaryAsFailed,
61
isAlreadyFailedLegacyErrorBoundary,
@@ -151,6 +151,20 @@ function throwException(
151
// Its effect list is no longer valid.
152
sourceFiber.firstEffect = sourceFiber.lastEffect = null;
153
154
+ // Check if there is lower priority work, regardless of whether it's pending.
155
+ // If so, it may have the effect of fixing the exception that was just thrown.
156
+ const latestPendingTime = root.latestPendingTime;
157
+ const latestSuspendedTime = root.latestSuspendedTime;
158
+ if (
159
+ renderExpirationTime !== latestPendingTime &&
160
+ renderExpirationTime !== latestSuspendedTime &&
161
+ renderExpirationTime !== Never
162
+ ) {
163
+ // There's lower priority work. Exit to suspend this render and retry at
164
+ // the lower priority.
165
+ return;
166
+ }
167
+
168
if (
169
enableSuspense &&
170
value !== null &&
@@ -206,7 +220,7 @@ function throwException(
220
221
if (renderExpirationTime === Never || msUntilTimeout > 0) {
222
// There's still time remaining.
209
- suspendRoot(root, thenable, msUntilTimeout, renderExpirationTime);
223
+ markTimeout(root, thenable, msUntilTimeout, renderExpirationTime);
224
const onResolveOrReject = () => {
225
retrySuspendedRoot(root, renderExpirationTime);
226
};
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+31
@@ -198,6 +198,37 @@ describe('ReactIncrementalErrorHandling', () => {
198
expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
199
});
200
201
+ it("retries at a lower priority if there's additional pending work", () => {
202
+ function App(props) {
203
+ if (props.isBroken) {
204
+ ReactNoop.yield('error');
205
+ throw new Error('Oops!');
206
+ }
207
+ ReactNoop.yield('success');
208
+ return <span prop="Everything is fine." />;
209
+ }
210
+
211
+ function onCommit() {
212
+ ReactNoop.yield('commit');
213
+ }
214
+
215
+ ReactNoop.render(<App isBroken={true} />, onCommit);
216
+ ReactNoop.expire(2000);
217
+ ReactNoop.render(<App isBroken={false} />, onCommit);
218
+
219
+ expect(ReactNoop.flush()).toEqual([
220
+ // The first render fails. But because there's a lower priority pending
221
+ // update, it doesn't throw.
222
+ 'error',
223
+ // Now we retry at the lower priority. This time it succeeds.
224
+ 'success',
225
+ // Nothing commits until the second update completes.
226
+ 'commit',
227
+ 'commit',
228
+ ]);
229
+ expect(ReactNoop.getChildren()).toEqual([span('Everything is fine.')]);
230
+ });
231
+
232
it('calls componentDidCatch multiple times for multiple errors', () => {
233
let id = 0;
234
class BadMount extends React.Component {