PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
← All changes | jetpack_vendor/automattic/jetpack-sync/src/class-dedicated-sender.php +79 -56 12.3.2 → 16.3-beta View file →
@@ -42,9 +42,9 @@
42 42 * What's the timeout for the request lock in seconds.
43 43 *
44 44 * 5 seconds as default value seems sane, but we might want to adjust that in the future.
45 45 */
46 - const DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT = 5;
46 + const DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT = 60;
47 47
48 48 /**
49 49 * The query parameter name to use when passing the current lock id.
50 50 */
@@ -84,9 +84,9 @@
84 84 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
85 85 return false;
86 86 }
87 87
88 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended
88 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended -- Only substring-matched against a fixed literal; never output or stored.
89 89 $check_url = self::prepare_url_for_dedicated_request_check( wp_unslash( $_SERVER['REQUEST_URI'] ) );
90 90 if ( strpos( $check_url, 'jetpack/v4/sync/spawn-sync' ) !== false ) {
91 91 return true;
92 92 }
@@ -96,13 +96,13 @@
96 96 * Sometimes, like when permalinks are disabled, the REST path is sent via the `rest_route` GET parameter.
97 97 * We want to check it too, to make sure we managed to cover more cases and be more certain we actually
98 98 * catch calls to the endpoint.
99 99 */
100 - if ( ! isset( $_GET['rest_route'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
100 + if ( ! isset( $_GET['rest_route'] ) || ! is_string( $_GET['rest_route'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
101 101 return false;
102 102 }
103 103
104 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended
104 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended -- Only substring-matched against a fixed literal; never output or stored.
105 105 $check_url = self::prepare_url_for_dedicated_request_check( wp_unslash( $_GET['rest_route'] ) );
106 106 if ( strpos( $check_url, 'jetpack/v4/sync/spawn-sync' ) !== false ) {
107 107 return true;
108 108 }
@@ -132,8 +132,12 @@
132 132 if ( $queue->size() === 0 ) {
133 133 return new WP_Error( 'empty_queue_' . $queue->id );
134 134 }
135 135
136 + if ( get_transient( Sender::TEMP_SYNC_DISABLE_TRANSIENT_NAME ) ) {
137 + return new WP_Error( 'sender_temporarily_disabled_while_pulling' );
138 + }
139 +
136 140 // Return early if we've gotten a retry-after header response that is not expired.
137 141 $retry_time = get_option( Actions::RETRY_AFTER_PREFIX . $queue->id );
138 142 if ( $retry_time && $retry_time >= microtime( true ) ) {
139 143 return new WP_Error( 'retry_after_' . $queue->id );
@@ -150,37 +154,8 @@
150 154 $queue_send_time_threshold = 30 * MINUTE_IN_SECONDS;
151 155
152 156 $queue_lag = $queue->lag();
153 157
154 - // Only check if we're failing to send events if the queue lag is longer than the threshold.
155 - if ( $queue_lag > $queue_send_time_threshold ) {
156 - /**
157 - * Check if Dedicated Sync is healthy and revert to Default Sync if such case is detected.
158 - */
159 - $last_successful_queue_send_time = get_option( Actions::LAST_SUCCESS_PREFIX . $queue->id, null );
160 -
161 - if ( $last_successful_queue_send_time === null ) {
162 - /**
163 - * No successful sync sending completed. This might be either a "new" sync site or a site that's totally stuck.
164 - */
165 - self::on_dedicated_sync_lag_not_sending_threshold_reached();
166 -
167 - return new WP_Error( 'dedicated_sync_not_sending', 'Dedicated Sync is not successfully sending events' );
168 - } else {
169 - /**
170 - * We have recorded a successful sending of events. Let's see if that is not too long ago in the past.
171 - */
172 - $time_since_last_succesful_send = time() - $last_successful_queue_send_time;
173 -
174 - if ( $time_since_last_succesful_send > $queue_send_time_threshold ) {
175 - // We haven't successfully sent stuff in more than 30 minutes. Revert to Default Sync
176 - self::on_dedicated_sync_lag_not_sending_threshold_reached();
177 -
178 - return new WP_Error( 'dedicated_sync_not_sending', 'Dedicated Sync is not successfully sending events' );
179 - }
180 - }
181 - }
182 -
183 158 /**
184 159 * Try to acquire a request lock, so we don't spawn multiple requests at the same time.
185 160 * This should prevent cases where sites might have limits on the amount of simultaneous requests.
186 161 */
@@ -188,8 +163,23 @@
188 163 if ( ! $request_lock ) {
189 164 return new WP_Error( 'dedicated_request_lock', 'Unable to acquire request lock' );
190 165 }
191 166
167 + /**
168 + * If the queue lag is bigger than the threshold, we want to check if Dedicated Sync is working correctly.
169 + * We will do by sending a test request and disabling Dedicated Sync if it's not working. We will also exit early
170 + * in case we send the test request since it is a blocking request.
171 + */
172 + if ( $queue_lag > $queue_send_time_threshold ) {
173 + if ( false === get_transient( self::DEDICATED_SYNC_CHECK_TRANSIENT ) ) {
174 + if ( ! self::can_spawn_dedicated_sync_request() ) {
175 + self::on_dedicated_sync_lag_not_sending_threshold_reached();
176 + return new WP_Error( 'dedicated_sync_not_sending', 'Dedicated Sync is not successfully sending events' );
177 + }
178 + return true;
179 + }
180 + }
181 +
192 182 $url = rest_url( 'jetpack/v4/sync/spawn-sync' );
193 183 $url = add_query_arg( 'time', time(), $url ); // Enforce Cache busting.
194 184 $url = add_query_arg( self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME, $request_lock, $url );
195 185
@@ -214,36 +204,52 @@
214 204 *
215 205 * To avoid spawning multiple requests at the same time, we need to have a quick lock that will
216 206 * allow only a single request to continue if we try to spawn multiple at the same time.
217 207 *
218 - * @return false|mixed|string
208 + * @return string|false
219 209 */
220 210 public static function try_lock_spawn_request() {
221 - $current_microtime = (string) microtime( true );
211 + $option_name = self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME;
212 + $expires_name = $option_name . '_expires';
213 + $ttl = self::DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT;
214 + $lock_id = wp_generate_uuid4();
215 + $now = microtime( true );
222 216
223 - $current_lock_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
217 + // Fast path: external object cache is atomic.
218 + if ( wp_using_ext_object_cache() ) {
219 + if ( wp_cache_add( $option_name, $lock_id, 'jetpack', $ttl ) ) {
220 + return $lock_id;
221 + }
222 + return false; // Worker already active
223 + }
224 224
225 - if ( ! empty( $current_lock_value ) ) {
226 - // Check if time has passed to overwrite the lock - min 5s?
227 - if ( is_numeric( $current_lock_value ) && ( ( $current_microtime - $current_lock_value ) < self::DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT ) ) {
228 - // Still in previous lock, quit
229 - return false;
230 - }
225 + global $wpdb;
231 226
232 - // If the value is not numeric (float/current time), we want to just overwrite it and continue.
227 + // 1) Check & clear expired lock (best effort; failure here is harmless)
228 + $expiry = (float) \Jetpack_Options::get_raw_option( $expires_name, 0 );
229 + if ( ! $expiry || $expiry < $now ) {
230 + // Either missing (edge case) or expired → clean up
231 + \Jetpack_Options::delete_raw_option( $option_name );
232 + \Jetpack_Options::delete_raw_option( $expires_name );
233 233 }
234 234
235 - // Update. We don't want it to autoload, as we want to fetch it right before the checks.
236 - \Jetpack_Options::update_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, $current_microtime, false );
237 - // Give some time for the update to happen
238 - usleep( wp_rand( 1000, 3000 ) );
235 + // 2) Atomic acquisition: INSERT IGNORE (succeeds only if the lock doesn't exist)
236 + $inserted = $wpdb->query( // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching --- Ensure atomicity.
237 + $wpdb->prepare(
238 + "INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload )
239 + VALUES ( %s, %s, 'no' )",
240 + $option_name,
241 + maybe_serialize( $lock_id )
242 + )
243 + );
239 244
240 - $updated_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
241 -
242 - if ( $updated_value === $current_microtime ) {
243 - return $current_microtime;
245 + if ( $inserted ) {
246 + // 3) We own the lock — store expiry separately
247 + \Jetpack_Options::update_raw_option( $expires_name, $now + $ttl, false );
248 + return $lock_id; // Success
244 249 }
245 250
251 + // Lock already present → normal state → do not spawn
246 252 return false;
247 253 }
248 254
249 255 /**
@@ -259,15 +265,26 @@
259 265 $lock_id = self::get_request_lock_id_from_request();
260 266 }
261 267
262 268 // If it's still not a valid lock_id, throw an error and let the lock process figure it out.
263 - if ( empty( $lock_id ) || ! is_numeric( $lock_id ) ) {
269 + if ( empty( $lock_id ) ) {
264 270 return new WP_Error( 'dedicated_request_lock_invalid', 'Invalid lock_id supplied for unlock' );
265 271 }
266 272
273 + if ( wp_using_ext_object_cache() ) {
274 + $cached = wp_cache_get( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, 'jetpack', true );
275 + if ( (string) $lock_id === $cached ) {
276 + wp_cache_delete( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, 'jetpack' );
277 +
278 + return true;
279 + }
280 +
281 + return false;
282 + }
283 +
284 + // If this is the flow that has the lock, let's release it so we can spawn other requests afterwards
267 285 $current_lock_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
268 286
269 - // If this is the flow that has the lock, let's release it so we can spawn other requests afterwards
270 287 if ( (string) $lock_id === $current_lock_value ) {
271 288 \Jetpack_Options::delete_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME );
272 289 return true;
273 290 }
@@ -281,9 +298,9 @@
281 298 * @return array|string|string[]|null
282 299 */
283 300 public static function get_request_lock_id_from_request() {
284 301 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
285 - if ( ! isset( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] ) || ! is_numeric( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] ) ) {
302 + if ( ! isset( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] ) ) {
286 303 return null;
287 304 }
288 305
289 306 // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
@@ -293,9 +310,9 @@
293 310 /**
294 311 * Test Sync spawning functionality by making a request to the
295 312 * Sync spawning endpoint and storing the result (status code) in a transient.
296 313 *
297 - * @since $$next_version$$
314 + * @since 1.34.0
298 315 *
299 316 * @return bool True if we got a successful response, false otherwise.
300 317 */
301 318 public static function can_spawn_dedicated_sync_request() {
@@ -348,9 +365,8 @@
348 365
349 366 $sender->send_action( 'jetpack_sync_flow_error_enable', $data );
350 367 }
351 368 }
352 -
353 369 return self::DEDICATED_SYNC_VALIDATION_STRING === $dedicated_sync_response_body;
354 370 }
355 371
356 372 /**
@@ -397,8 +413,15 @@
397 413 if ( $check_transient ) {
398 414 // Something happened and Dedicated Sync should not be automatically re-enabled.
399 415 return false;
400 416 }
417 + }
418 +
419 + $current_setting = Settings::is_dedicated_sync_enabled();
420 +
421 + // No need to update if current setting matches header value.
422 + if ( $current_setting === (bool) $dedicated_sync_enabled ) {
423 + return $current_setting;
401 424 }
402 425
403 426 Settings::update_settings(
404 427 array(