Core Data: Discard entity record responses superseded by a newer request - #81846
Core Data: Discard entity record responses superseded by a newer request#81846adamsilverstein wants to merge 5 commits into
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: +67 B (0%) Total Size: 7.91 MB 📦 View Changed
|
|
Related:
Centralized resolution invalidation seems less error prone to me. Otherwise it's too easy to make this same mistake in other places as well, which will be confusing for users (stale data in one place, current data in another) |
`invalidateResolution` starts a second request for a record while the first one may still be in flight, and the store kept whichever response was delivered last. A response whose body was read from the database before an update could therefore overwrite the updated record, with nothing left to invalidate afterwards. Client-side media uploads hit this: the attachment record fetched while sub-sizes are still being generated carries an empty `media_details.sizes`, and when that response lands after the post-upload refetch it hides the Image block's Resolution control for the rest of the session. Existing invalidation cannot fix it, because the stale response arrives after the invalidation has already been acted on. Have `getEntityRecord` drop a response once a newer response for the same request has been received. Only identical requests are compared, so a request for a different set of `_fields` never discards another one's data, and a newer request that fails leaves the older response to be used as before. Fixes #81844.
01a8a71 to
c2a8e66
Compare
…-stale-attachment-record # Conflicts: # packages/core-data/CHANGELOG.md
|
I asked Claude to work through your feedback, here is what came back: Agreed, and following that thread turned up the real problem. Centralized invalidation is already in place - So the fix moved down a layer instead of sideways. It stays conservative on both edges: only identical requests are compared, so a different Verified red-then-green at both levels: a new
|
A batch upload has several attachment records resolving at once. Prove that a response is only ever superseded by a newer response for the same request, so one record in flight is never discarded because a different one was received first.
…-stale-attachment-record
|
Flaky tests detected in de7cfa9. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/33114719072 three users concurrently edit a large post with diverse blocks in
|
Claude investigated the report and wrote the fix and tests:
Fixes #81844
What?
getEntityRecordnow discards a REST response once a newer response for the same request has already been received.An earlier revision of this PR fixed the reported symptom inside the Image block. Following @swissspidy's review that approach is gone, and the fix moved to the shared layer where the defect actually lives.
Why?
invalidateResolutionstarts a second request for a record while the first one may still be in flight, and the store keeps whichever response is delivered last. A response whose body was read from the database before an update can therefore overwrite the updated record - and once that happens nothing is left to invalidate.Client-side media uploads hit this. The attachment is created first, its sub-sizes are sideloaded, and only the final
finalizerequest writesmedia_details.sizes. The Image block starts resolvinggetEntityRecord( 'postType', 'attachment', id, { context: 'view' } )as soon as the attachment ID lands in its attributes, so that GET returns"sizes": {}. On a busy server it can be delivered after the post-upload invalidation refetch, replacing the finalized record with the empty one -imageSizeOptionsstays empty and the Resolution control never renders again. That matches the field evidence in the issue: a successfulGET /wp/v2/media/{id}?context=viewwhose body contains"sizes": {}, and a front end whosesrcsetproves every sub-size was generated.Playground serializes PHP requests, so responses cannot arrive out of order there - which is why the issue only reproduces on real installations.
Existing invalidation cannot fix this.
mediaUploadOnSuccessalready invalidates the attachment record centrally after an upload, but the stale response arrives after that invalidation has been acted on. The ordering is the bug.How?
Each record request reserves a sequence number before it goes out. When its response comes back, it is dropped if a newer response for the same request has already been written to the store.
Two properties keep this conservative:
_fields, or a different context, never discards another one's data.Fixing it here means every consumer benefits - Gallery, Cover, Media & Text and the media modal read the same records and could hit the same stale-clobber.
Testing Instructions
Two tests, both verified to fail without the change and pass with it.
Unit - the ordering guard in isolation:
E2e - the reported bug, reproduced deterministically. It holds the response of the attachment GET issued before
finalize(reading its stale body immediately), delivers it only after the finalized record has been cached, then checks that the cached record keeps its sizes and the Resolution control is still visible:Manual reproduction needs an unlucky response ordering, so a fast local server usually will not show it - the linked issue has the field evidence.
Follow-up
getEntityRecordshas the same last-delivered-wins behaviour for lists, and matters most for batch uploads. Left alone here to keep this change reviewable, tracked separately in #81885.AI Use
Code and description both written with 🤖 Claude Code. I will review and test.