Skip to content

Track in-flight operations by executor lifetime, not request-cache subscribers#5360

Open
jonreading81 wants to merge 2 commits into
facebook:mainfrom
jonreading81:jr-fix-5357-operation-retention
Open

Track in-flight operations by executor lifetime, not request-cache subscribers#5360
jonreading81 wants to merge 2 commits into
facebook:mainfrom
jonreading81:jr-fix-5357-operation-retention

Conversation

@jonreading81

@jonreading81 jonreading81 commented Jul 16, 2026

Copy link
Copy Markdown

Summary

Closes a fragment-to-owner correlation gap that surfaces on streaming responses when the request-cache subject is drained between Suspense render passes while the underlying OperationExecutor is still emitting incremental payloads. In that window, getPendingOperationsForFragment returns null, useFragment reads partial data instead of suspending, and any consumer that reads a non-null field on the missing subtree crashes the render.

Introduces a per-environment registry of operation completion promises tied to OperationExecutor lifetime, independent of the request-cache subject and its subscriber count. Behavior change is gated behind a new RelayFeatureFlags.ENABLE_IN_FLIGHT_OPERATION_CORRELATION (default off) for gradual rollout.

Mechanism (before this PR)

fetchQueryDeduped caches a per-identifier subject and tears it down when subscriber count hits 0. Under React 19 streaming SSR, that cleanup fires between render passes for a query still emitting @defer chunks — subject torn down while the executor is alive. getPromiseForActiveRequest returns null (cache empty); RelayOperationTracker.getPendingOperationsAffectingOwner returns null (no other operation writes to the owner's records). getPendingOperationsForFragment returns null. useFragment reads the partial snapshot.

Discussion and trace: #5357.

What this PR does

  • RelayModernEnvironment — adds _inFlightOperationCompletions: Map<string, {promise, resolve}> and a public getPromiseForInFlightOperation(id) accessor.
  • OperationExecutor — registers a completion promise on construction; resolves and removes it in cancel().
  • getPendingOperationsForFragment — after getPromiseForActiveRequest and RelayOperationTracker both come up null, consults the in-flight registry as a last-resort correlation. Guarded by the feature flag; the registry entry's presence IS the "in-flight" signal (populated on OperationExecutor construction, cleared atomically in cancel()). We deliberately do NOT additionally gate on isRequestActive — that reads _operationExecutions, which flips to 'inactive' while _state === 'loading_final' (no pending module payloads) BEFORE the network stream completes and the completion map is cleared. Fragments reading during that window would otherwise fall through to partial-data reads.
  • RelayFeatureFlags — adds ENABLE_IN_FLIGHT_OPERATION_CORRELATION: boolean (default false).
  • MultiActorEnvironment / ActorSpecificEnvironment / IEnvironment / IMultiActorEnvironment — mirror the accessor for symmetry.
  • Tests — three unit tests on environment.getPromiseForInFlightOperation covering never-started / in-flight / completed; two integration tests in useFragment-WithOperationTrackerSuspense-test.js: the first mirrors the existing suspense pattern (fragment reads missing data, executor alive, cache empty → suspends → resolves on mock.complete); the second directly asserts the gate skips isRequestActive — with the executor still in flight but isRequestActive mocked to false, the fragment must still suspend on the completion promise.

Design notes

  • Fallback placed in getPendingOperationsForFragment, not getPromiseForActiveRequest, so mutation-driven correlations that legitimately resolve through RelayOperationTracker are not short-circuited (an earlier attempt at the wrong level broke a FragmentResource plural-mutation test).
  • Registry entries are populated on OperationExecutor construction and removed in cancel(), so lifecycle paths that already reach cancel() clean up naturally. No new leak surface; same invariant as the sibling _operationExecutions map.
  • ExecuteConfig gains one field (inFlightOperationCompletions), threaded through by all env.execute* paths — same shape as the existing operationExecutions param.

Test plan

  • yarn jest packages/relay-runtime — 2279/2279 (28 snapshots)
  • yarn jest packages/react-relay — 1274/1274 (20 skipped)
  • Focused: useFragment-WithOperationTrackerSuspense-test.js — 6/6 including the two new integration tests.
  • Validated against a real @defer-heavy production codebase with the same shape (Mixcloud, mid-stream @defer + Suspense render loops) — no regressions across the E2E suite with the flag enabled.

@meta-cla

meta-cla Bot commented Jul 16, 2026

Copy link
Copy Markdown

Hi @jonreading81!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@jonreading81
jonreading81 force-pushed the jr-fix-5357-operation-retention branch from 8b860cc to 26de1b0 Compare July 16, 2026 12:08
…bscribers

Introduce a per-environment map of operation completion promises keyed by
request identifier. OperationExecutor writes an entry on construction and
resolves + removes it on cancel(). Environment exposes it via
getPromiseForInFlightOperation.

getPendingOperationsForFragment consults it as a last-resort correlation
when neither the fetch-cache subject nor the operation tracker holds a
promise. This closes a fragment-to-owner correlation gap on streaming
responses where the request-cache subject is drained between render
passes while incremental payloads are still being processed.
@jonreading81
jonreading81 force-pushed the jr-fix-5357-operation-retention branch 2 times, most recently from f8d9575 to 1192e91 Compare July 20, 2026 10:42
`_operationExecutions` flips to `'inactive'` while `_state === 'loading_final'`
(with no pending module payloads) — before the network stream completes and
`_inFlightOperationCompletions` is cleared in `cancel()`. Under incremental
delivery, fragments reading during that "final-but-not-complete" window fall
through to partial-data reads instead of suspending on the still-valid
completion promise, defeating this correlation. Gate solely on the completion
map entry's presence (via `getPromiseForInFlightOperation`) — populated in the
Executor constructor and cleared atomically when `_complete` fires — which
IS the correct in-flight signal for this branch.

Adds a test in useFragment-WithOperationTrackerSuspense-test.js exercising the
mid-stream window where isRequestActive returns false while the completion
promise is still valid; the fragment must suspend on that promise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jonreading81

Copy link
Copy Markdown
Author

The CLA has been signed — could the bot re-check when it gets a chance? Thanks.

@meta-cla meta-cla Bot added the CLA Signed label Jul 21, 2026
@meta-cla

meta-cla Bot commented Jul 21, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant