Repository navigation
fix(sync): Break the sync loop that can overload a server (#1301) #1328
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d017a48
ff83557
9153887
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2787,15 +2787,63 @@ public function build_cached_meta( $post_id, $key, $single ) { | |
| */ | ||
| public function update_post_meta( $post_id, $key, $data ) { | ||
|
|
||
| $meta = $this->get_post_meta( $post_id ); | ||
| if ( ! isset( $meta[ $key ] ) ) { | ||
| $meta[ $key ] = ''; | ||
| } | ||
| // The `_cloudinary` meta is a single serialized array shared by every sync type, so | ||
| // this is a read-modify-write: fetch the whole thing, change one key, write the whole | ||
| // thing back. Two of these running concurrently for the same attachment (e.g. a | ||
| // bulk-sync thread and an autosync thread both reaching it, see GitHub issue #1301) can | ||
| // race -- whichever writes last overwrites the other's change outright, silently. Use | ||
| // WP's compare-and-swap (the $prev_value arg) to detect that and retry against the | ||
| // now-current value instead of clobbering it. | ||
| $max_attempts = 3; | ||
| for ( $attempt = 0; $attempt < $max_attempts; $attempt++ ) { | ||
| if ( $attempt > 0 ) { | ||
| // Only before a retry, not the first attempt: the compare-and-swap below | ||
| // ($prev_value) is what catches a stale read, by failing against the DB's actual | ||
| // current value regardless of what's cached. Forcing a fresh read up front would | ||
| // cost every single meta write here -- and there are many per sync -- a full | ||
| // reload for no benefit, since nothing has failed yet to justify distrusting the | ||
| // cache. | ||
| wp_cache_delete( $post_id, 'post_meta' ); | ||
| } | ||
|
|
||
| if ( $meta[ $key ] !== $data ) { | ||
| // Read before checking existence: when the row is missing, get_post_meta() below | ||
| // runs the `cloudinary_migrate_legacy_meta` filter, whose callbacks can create this | ||
| // row themselves as a side effect (e.g. legacy-plugin migration). Checking existence | ||
| // first would miss that and cost an extra, avoidable retry. | ||
| $meta = $this->get_post_meta( $post_id ); | ||
| $exists = metadata_exists( 'post', $post_id, Sync::META_KEYS['cloudinary'] ); | ||
| if ( isset( $meta[ $key ] ) && $meta[ $key ] === $data ) { | ||
| return true; // Already set to this value, including by a racing write. | ||
| } | ||
| $prev = $meta; | ||
| $meta[ $key ] = $data; | ||
|
|
||
| if ( $exists ) { | ||
| // Note: update_metadata() only applies $prev_value (and so only detects a race) | ||
| // when it's non-empty. If `_cloudinary` currently holds an empty array (e.g. | ||
| // right after meta_cleanup) or something that didn't come back as an array, this | ||
| // falls through to an unconditional write instead -- still correct, just unable | ||
| // to tell a race apart from an ordinary write in that specific case. | ||
| $result = update_post_meta( $post_id, Sync::META_KEYS['cloudinary'], $meta, $prev ); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: The CAS is disabled when |
||
| } else { | ||
| // No row yet: add it uniquely so a concurrent first write can't be silently | ||
| // dropped the way an unconditional update_post_meta() would allow. | ||
| $result = add_post_meta( $post_id, Sync::META_KEYS['cloudinary'], $meta, true ); | ||
| } | ||
|
|
||
| if ( false !== $result ) { | ||
| return $result; | ||
| } | ||
| // Lost the race to a concurrent write; loop and retry against the new state. | ||
| } | ||
|
|
||
| // Still contended after retries: write unconditionally rather than looping forever. | ||
| // This can still lose a concurrent change, but only after genuinely sustained | ||
| // contention, not on the first overlap. | ||
| wp_cache_delete( $post_id, 'post_meta' ); | ||
| $meta = $this->get_post_meta( $post_id ); | ||
| $meta[ $key ] = $data; | ||
|
|
||
| return update_post_meta( $post_id, Sync::META_KEYS['cloudinary'], $meta ); | ||
| } | ||
|
|
||
|
|
@@ -2809,11 +2857,35 @@ public function update_post_meta( $post_id, $key, $data ) { | |
| */ | ||
| public function delete_post_meta( $post_id, $key ) { | ||
|
|
||
| $meta = $this->get_post_meta( $post_id ); | ||
| if ( isset( $meta[ $key ] ) ) { | ||
| // Same shared-blob read-modify-write as update_post_meta() above, so apply the same | ||
| // compare-and-swap retry against concurrent writers. | ||
| $max_attempts = 3; | ||
| for ( $attempt = 0; $attempt < $max_attempts; $attempt++ ) { | ||
| if ( $attempt > 0 ) { | ||
| // See the matching note in update_post_meta(): only before a retry, once the | ||
| // compare-and-swap below has actually failed against the DB's current value. | ||
| wp_cache_delete( $post_id, 'post_meta' ); | ||
| } | ||
|
|
||
| $meta = $this->get_post_meta( $post_id ); | ||
| if ( ! isset( $meta[ $key ] ) ) { | ||
| return true; // Already gone, including by a racing write. | ||
| } | ||
| $prev = $meta; | ||
| unset( $meta[ $key ] ); | ||
|
|
||
| $result = update_post_meta( $post_id, Sync::META_KEYS['cloudinary'], $meta, $prev ); | ||
| if ( false !== $result ) { | ||
| return $result; | ||
| } | ||
| // Lost the race to a concurrent write; loop and retry against the new state. | ||
| } | ||
|
|
||
| // Still contended after retries: write unconditionally rather than looping forever. | ||
| wp_cache_delete( $post_id, 'post_meta' ); | ||
| $meta = $this->get_post_meta( $post_id ); | ||
| unset( $meta[ $key ] ); | ||
|
|
||
| return update_post_meta( $post_id, Sync::META_KEYS['cloudinary'], $meta ); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
$existsis read beforeget_post_meta(). When the row is missing,get_post_meta()appliescloudinary_migrate_legacy_meta.Upgrade::migrate_legacy_meta()can write_cloudinaryitself (lines 229 and 259). In that case,add_post_meta( ..., true )fails and the first attempt is lost. The retry recovers, but if you swap the two lines, this extra attempt does not occur.