PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.1
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 13.8.3 13.9.2 14.0.1 All 504 releases
← All changes | jetpack_vendor/automattic/jetpack-sync/src/class-queue.php +132 -176 12.2.3 → 16.3-a.1 View file →
@@ -6,8 +6,10 @@
6 6 */
7 7
8 8 namespace Automattic\Jetpack\Sync;
9 9
10 +use Automattic\Jetpack\Sync\Queue\Queue_Storage_Options;
11 +use Automattic\Jetpack\Sync\Queue\Queue_Storage_Table;
10 12 use WP_Error;
11 13
12 14 /**
13 15 * A persistent queue that can be flushed in increments of N items,
@@ -37,8 +39,17 @@
37 39 */
38 40 public $random_int;
39 41
40 42 /**
43 + * Queue Storage instance where we'll store the queue items.
44 + *
45 + * For now, it's only the Options table. To be updated to include the Custom table in future updates.
46 + *
47 + * @var Queue_Storage_Options|Queue_Storage_Table|null
48 + */
49 + public $queue_storage = null;
50 +
51 + /**
41 52 * Queue constructor.
42 53 *
43 54 * @param string $id Name of the queue.
44 55 */
@@ -45,8 +56,18 @@
45 56 public function __construct( $id ) {
46 57 $this->id = str_replace( '-', '_', $id ); // Necessary to ensure we don't have ID collisions in the SQL.
47 58 $this->row_iterator = 0;
48 59 $this->random_int = wp_rand( 1, 1000000 );
60 +
61 + /**
62 + * If the Custom queue table is enabled - let's use it as a backend. Otherwise, fall back to the Options table.
63 + */
64 + if ( Settings::is_custom_queue_table_enabled() ) {
65 + $this->queue_storage = new Queue_Storage_Table( $this->id );
66 + } else {
67 + // Initialize the storage with the Options table backend. To be changed in subsequent updates to include the logic to switch to Custom Table.
68 + $this->queue_storage = new Queue_Storage_Options( $this->id );
69 + }
49 70 }
50 71
51 72 /**
52 73 * Add a single item to the queue.
@@ -51,16 +72,17 @@
51 72 /**
52 73 * Add a single item to the queue.
53 74 *
54 75 * @param object $item Event object to add to queue.
76 + *
77 + * @return bool|WP_Error
55 78 */
56 79 public function add( $item ) {
57 - global $wpdb;
58 80 $added = false;
59 81
60 82 // If empty, don't add.
61 83 if ( empty( $item ) ) {
62 - return;
84 + return false;
63 85 }
64 86
65 87 // Attempt to serialize data, if an exception (closures) return early.
66 88 try {
@@ -65,24 +87,18 @@
65 87 // Attempt to serialize data, if an exception (closures) return early.
66 88 try {
67 89 $item = serialize( $item ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
68 90 } catch ( \Exception $ex ) {
69 - return;
91 + return new WP_Error( 'queue_unable_to_serialize', 'Unable to serialize item' );
70 92 }
71 93
72 94 // This basically tries to add the option until enough time has elapsed that
73 95 // it has a unique (microtime-based) option key.
74 96 while ( ! $added ) {
75 - $rows_added = $wpdb->query(
76 - $wpdb->prepare(
77 - "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES (%s, %s,%s)",
78 - $this->get_next_data_row_option_name(),
79 - $item,
80 - 'no'
81 - )
82 - );
83 - $added = ( 0 !== $rows_added );
97 + $added = $this->queue_storage->insert_item( $this->get_next_data_row_option_name(), $item );
84 98 }
99 +
100 + return $added;
85 101 }
86 102
87 103 /**
88 104 * Insert all the items in a single SQL query. May be subject to query size limits!
@@ -91,40 +107,22 @@
91 107 *
92 108 * @return bool|\WP_Error
93 109 */
94 110 public function add_all( $items ) {
95 - global $wpdb;
111 + // TODO check and figure out if it's used at all and if we can optimize it.
96 112 $base_option_name = $this->get_next_data_row_option_name();
97 113
98 - $query = "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES ";
114 + $rows_added = $this->queue_storage->add_all( $items, $base_option_name );
99 115
100 - $rows = array();
101 - $count_items = count( $items );
102 - for ( $i = 0; $i < $count_items; ++$i ) {
103 - // skip empty items.
104 - if ( empty( $items[ $i ] ) ) {
105 - continue;
106 - }
107 - try {
108 - $option_name = esc_sql( $base_option_name . '-' . $i );
109 - $option_value = esc_sql( serialize( $items[ $i ] ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
110 - $rows[] = "('$option_name', '$option_value', 'no')";
111 - } catch ( \Exception $e ) {
112 - // Item cannot be serialized so skip.
113 - continue;
114 - }
115 - }
116 -
117 - $rows_added = $wpdb->query( $query . implode( ',', $rows ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
118 -
119 116 if ( count( $items ) !== $rows_added ) {
120 117 return new WP_Error( 'row_count_mismatch', "The number of rows inserted didn't match the size of the input array" );
121 118 }
119 +
122 120 return true;
123 121 }
124 122
125 123 /**
126 - * Get the front-most item on the queue without checking it out.
124 + * Get the front-most items on the queue without checking them out.
127 125 *
128 126 * @param int $count Number of items to return when looking at the items.
129 127 *
130 128 * @return array
@@ -138,8 +136,24 @@
138 136 return array();
139 137 }
140 138
141 139 /**
140 + * Get the last-added items on the queue without checking them out.
141 + *
142 + * @param int $count Number of items to return when looking at the items.
143 + *
144 + * @return array
145 + */
146 + public function peek_newest( $count = 1 ) {
147 + $items = $this->fetch_items( $count, 'DESC' );
148 + if ( $items ) {
149 + return Utils::get_item_values( $items );
150 + }
151 +
152 + return array();
153 + }
154 +
155 + /**
142 156 * Gets items with particular IDs.
143 157 *
144 158 * @param array $item_ids Array of item IDs to retrieve.
145 159 *
@@ -158,37 +172,14 @@
158 172 * Gets the queue lag.
159 173 * Lag is the difference in time between the age of the oldest item
160 174 * (aka first or frontmost item) and the current time.
161 175 *
162 - * @param microtime $now The current time in microtime.
176 + * @param float $now The current time in microtime.
163 177 *
164 - * @return float|int|mixed|null
178 + * @return float
165 179 */
166 180 public function lag( $now = null ) {
167 - global $wpdb;
168 -
169 - $first_item_name = $wpdb->get_var(
170 - $wpdb->prepare(
171 - "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT 1",
172 - "jpsq_{$this->id}-%"
173 - )
174 - );
175 -
176 - if ( ! $first_item_name ) {
177 - return 0;
178 - }
179 -
180 - if ( null === $now ) {
181 - $now = microtime( true );
182 - }
183 -
184 - // Break apart the item name to get the timestamp.
185 - $matches = null;
186 - if ( preg_match( '/^jpsq_' . $this->id . '-(\d+\.\d+)-/', $first_item_name, $matches ) ) {
187 - return $now - (float) $matches[1];
188 - } else {
189 - return 0;
190 - }
181 + return (float) $this->queue_storage->get_lag( $now );
191 182 }
192 183
193 184 /**
194 185 * Resets the queue.
@@ -193,16 +184,11 @@
193 184 /**
194 185 * Resets the queue.
195 186 */
196 187 public function reset() {
197 - global $wpdb;
198 188 $this->delete_checkout_id();
199 - $wpdb->query(
200 - $wpdb->prepare(
201 - "DELETE FROM $wpdb->options WHERE option_name LIKE %s",
202 - "jpsq_{$this->id}-%"
203 - )
204 - );
189 +
190 + $this->queue_storage->clear_queue();
205 191 }
206 192
207 193 /**
208 194 * Return the size of the queue.
@@ -209,16 +195,9 @@
209 195 *
210 196 * @return int
211 197 */
212 198 public function size() {
213 - global $wpdb;
214 -
215 - return (int) $wpdb->get_var(
216 - $wpdb->prepare(
217 - "SELECT count(*) FROM $wpdb->options WHERE option_name LIKE %s",
218 - "jpsq_{$this->id}-%"
219 - )
220 - );
199 + return $this->queue_storage->get_item_count();
221 200 }
222 201
223 202 /**
224 203 * Lets you know if there is any items in the queue.
@@ -227,17 +206,9 @@
227 206 *
228 207 * @return bool
229 208 */
230 209 public function has_any_items() {
231 - global $wpdb;
232 - $value = $wpdb->get_var(
233 - $wpdb->prepare(
234 - "SELECT exists( SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s )",
235 - "jpsq_{$this->id}-%"
236 - )
237 - );
238 -
239 - return ( '1' === $value );
210 + return $this->size() > 0;
240 211 }
241 212
242 213 /**
243 214 * Used to checkout the queue.
@@ -243,9 +214,9 @@
243 214 * Used to checkout the queue.
244 215 *
245 216 * @param int $buffer_size Size of the buffer to checkout.
246 217 *
247 - * @return Automattic\Jetpack\Sync\Queue_Buffer|bool|int|\WP_Error
218 + * @return \Automattic\Jetpack\Sync\Queue_Buffer|bool|int|\WP_Error
248 219 */
249 220 public function checkout( $buffer_size ) {
250 221 if ( $this->get_checkout_id() ) {
251 222 return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' );
@@ -250,9 +221,10 @@
250 221 if ( $this->get_checkout_id() ) {
251 222 return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' );
252 223 }
253 224
254 - $buffer_id = uniqid();
225 + // TODO check if adding a prefix is going to be a problem
226 + $buffer_id = uniqid( '', true );
255 227
256 228 $result = $this->set_checkout_id( $buffer_id );
257 229
258 230 if ( ! $result || is_wp_error( $result ) ) {
@@ -268,11 +240,9 @@
268 240 if ( count( $items ) === 0 ) {
269 241 return false;
270 242 }
271 243
272 - $buffer = new Queue_Buffer( $buffer_id, array_slice( $items, 0, $buffer_size ) );
273 -
274 - return $buffer;
244 + return new Queue_Buffer( $buffer_id, array_slice( $items, 0, $buffer_size ) );
275 245 }
276 246
277 247 /**
278 248 * Given a list of items return the items ids.
@@ -290,11 +260,11 @@
290 260 );
291 261 }
292 262
293 263 /**
294 - * Pop elements from the queue.
264 + * Remove the oldest items from the queue.
295 265 *
296 - * @param int $limit Number of items to pop from the queue.
266 + * @param int $limit Number of items to remove from the queue.
297 267 *
298 268 * @return array|object|null
299 269 */
300 270 public function pop( $limit ) {
@@ -299,8 +269,12 @@
299 269 */
300 270 public function pop( $limit ) {
301 271 $items = $this->fetch_items( $limit );
302 272
273 + if ( ! $items ) {
274 + return array();
275 + }
276 +
303 277 $ids = $this->get_ids( $items );
304 278
305 279 $this->delete( $ids );
306 280
@@ -307,8 +281,29 @@
307 281 return $items;
308 282 }
309 283
310 284 /**
285 + * Remove the newest items from the queue.
286 + *
287 + * @param int $limit Number of items to remove from the queue.
288 + *
289 + * @return array|object|null
290 + */
291 + public function pop_newest( $limit ) {
292 + $items = $this->fetch_items( $limit, 'DESC' );
293 +
294 + if ( ! $items ) {
295 + return array();
296 + }
297 +
298 + $ids = $this->get_ids( $items );
299 +
300 + $this->delete( $ids );
301 +
302 + return $items;
303 + }
304 +
305 + /**
311 306 * Get the items from the queue with a memory limit.
312 307 *
313 308 * This checks out rows until it either empties the queue or hits a certain memory limit
314 309 * it loads the sizes from the DB first so that it doesn't accidentally
@@ -318,9 +313,9 @@
318 313 *
319 314 * @param int $max_memory (bytes) Maximum memory threshold.
320 315 * @param int $max_buffer_size Maximum buffer size (number of items).
321 316 *
322 - * @return Automattic\Jetpack\Sync\Queue_Buffer|bool|int|\WP_Error
317 + * @return \Automattic\Jetpack\Sync\Queue_Buffer|bool|int|\WP_Error
323 318 */
324 319 public function checkout_with_memory_limit( $max_memory, $max_buffer_size = 500 ) {
325 320 if ( $this->get_checkout_id() ) {
326 321 return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' );
@@ -325,9 +320,9 @@
325 320 if ( $this->get_checkout_id() ) {
326 321 return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' );
327 322 }
328 323
329 - $buffer_id = uniqid();
324 + $buffer_id = uniqid( '', true );
330 325
331 326 $result = $this->set_checkout_id( $buffer_id );
332 327
333 328 if ( ! $result || is_wp_error( $result ) ) {
@@ -333,33 +328,24 @@
333 328 if ( ! $result || is_wp_error( $result ) ) {
334 329 return $result;
335 330 }
336 331
337 - // Get the map of buffer_id -> memory_size.
338 - global $wpdb;
332 + // How much memory is currently being used by the items.
333 + $total_memory = 0;
339 334
340 - $items_with_size = $wpdb->get_results(
341 - $wpdb->prepare(
342 - "SELECT option_name AS id, LENGTH(option_value) AS value_size FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT %d",
343 - "jpsq_{$this->id}-%",
344 - $max_buffer_size
345 - ),
346 - OBJECT
347 - );
335 + // Store the items to return
336 + $items = array();
348 337
349 - if ( ! is_countable( $items_with_size ) ) {
350 - return false;
351 - }
338 + $current_items_ids = $this->queue_storage->get_items_ids_with_size( $max_buffer_size - count( $items ) );
352 339
353 - if ( count( $items_with_size ) === 0 ) {
340 + // If no valid items are returned or no items are returned, continue.
341 + if ( ! is_countable( $current_items_ids ) || count( $current_items_ids ) === 0 ) {
354 342 return false;
355 343 }
356 344
357 - $total_memory = 0;
358 - $max_item_id = $items_with_size[0]->id;
359 - $min_item_id = $max_item_id;
345 + $item_ids_to_fetch = array();
360 346
361 - foreach ( $items_with_size as $id => $item_with_size ) {
347 + foreach ( $current_items_ids as $id => $item_with_size ) {
362 348 $total_memory += $item_with_size->value_size;
363 349
364 350 // If this is the first item and it exceeds memory, allow loop to continue
365 351 // we will exit on the next iteration instead.
@@ -366,42 +352,44 @@
366 352 if ( $total_memory > $max_memory && $id > 0 ) {
367 353 break;
368 354 }
369 355
370 - $max_item_id = $item_with_size->id;
356 + $item_ids_to_fetch[] = $item_with_size->id;
371 357 }
372 358
373 - $query = $wpdb->prepare(
374 - "SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name >= %s and option_name <= %s ORDER BY option_name ASC",
375 - $min_item_id,
376 - $max_item_id
377 - );
359 + $current_items = $this->queue_storage->fetch_items_by_ids( $item_ids_to_fetch );
378 360
379 - $items = $wpdb->get_results( $query, OBJECT ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
380 - $items_count = is_countable( $items ) ? count( $items ) : 0;
361 + $items_count = is_countable( $current_items ) ? count( $current_items ) : 0;
381 362
382 363 if ( $items_count > 0 ) {
383 - foreach ( $items as $item ) {
364 + /**
365 + * Save some memory by moving things one by one to the array of items being returned, instead of
366 + * unserializing all and then merging them with other items.
367 + *
368 + * PHPCS ignore is because this is the expected behavior - we're assigning a variable in the condition part of the loop.
369 + */
370 + // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
371 + while ( ( $current_item = array_shift( $current_items ) ) !== null ) {
384 372 // @codingStandardsIgnoreStart
385 - $item->value = @unserialize( $item->value );
373 + $current_item->value = @unserialize( $current_item->value );
386 374 // @codingStandardsIgnoreEnd
375 + $items[] = $current_item;
387 376 }
388 377 }
389 378
390 - if ( $items_count === 0 ) {
379 + if ( count( $items ) === 0 ) {
391 380 $this->delete_checkout_id();
381 +
392 382 return false;
393 383 }
394 384
395 - $buffer = new Queue_Buffer( $buffer_id, $items );
396 -
397 - return $buffer;
385 + return new Queue_Buffer( $buffer_id, $items );
398 386 }
399 387
400 388 /**
401 389 * Check in the queue.
402 390 *
403 - * @param Automattic\Jetpack\Sync\Queue_Buffer $buffer Queue_Buffer object.
391 + * @param \Automattic\Jetpack\Sync\Queue_Buffer $buffer Queue_Buffer object.
404 392 *
405 393 * @return bool|\WP_Error
406 394 */
407 395 public function checkin( $buffer ) {
@@ -418,10 +406,10 @@
418 406
419 407 /**
420 408 * Close the buffer.
421 409 *
422 - * @param Automattic\Jetpack\Sync\Queue_Buffer $buffer Queue_Buffer object.
423 - * @param null|array $ids_to_remove Ids to remove from the queue.
410 + * @param \Automattic\Jetpack\Sync\Queue_Buffer $buffer Queue_Buffer object.
411 + * @param null|array $ids_to_remove Ids to remove from the queue.
424 412 *
425 413 * @return bool|\WP_Error
426 414 */
427 415 public function close( $buffer, $ids_to_remove = null ) {
@@ -458,13 +446,12 @@
458 446 private function delete( $ids ) {
459 447 if ( array() === $ids ) {
460 448 return 0;
461 449 }
462 - global $wpdb;
463 - $sql = "DELETE FROM $wpdb->options WHERE option_name IN (" . implode( ', ', array_fill( 0, count( $ids ), '%s' ) ) . ')';
464 - $query = call_user_func_array( array( $wpdb, 'prepare' ), array_merge( array( $sql ), $ids ) );
465 450
466 - return $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
451 + $this->queue_storage->delete_items_by_ids( $ids );
452 +
453 + return true;
467 454 }
468 455
469 456 /**
470 457 * Flushes all items from the queue.
@@ -573,11 +560,14 @@
573 560 )
574 561 );
575 562
576 563 if ( $checkout_value ) {
577 - list( $checkout_id, $timestamp ) = explode( ':', $checkout_value );
578 - if ( (int) $timestamp > time() ) {
579 - return $checkout_id;
564 + $parts = explode( ':', $checkout_value, 2 );
565 + if ( count( $parts ) === 2 ) {
566 + list( $checkout_id, $timestamp ) = $parts;
567 + if ( (int) $timestamp > time() ) {
568 + return $checkout_id;
569 + }
580 570 }
581 571 }
582 572
583 573 return false;
@@ -662,32 +652,17 @@
662 652 /**
663 653 * Return the items in the queue.
664 654 *
665 655 * @param null|int $limit Limit to the number of items we fetch at once.
656 + * @param string $order Sort direction for the items. Accepts 'ASC' or 'DESC'.
657 + * Any other value will be treated as 'ASC'.
666 658 *
667 659 * @return array|object|null
668 660 */
669 - private function fetch_items( $limit = null ) {
670 - global $wpdb;
661 + private function fetch_items( $limit = null, $order = 'ASC' ) {
662 + $order = 'DESC' === $order ? 'DESC' : 'ASC';
671 663
672 - if ( $limit ) {
673 - $items = $wpdb->get_results(
674 - $wpdb->prepare(
675 - "SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT %d",
676 - "jpsq_{$this->id}-%",
677 - $limit
678 - ),
679 - OBJECT
680 - );
681 - } else {
682 - $items = $wpdb->get_results(
683 - $wpdb->prepare(
684 - "SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC",
685 - "jpsq_{$this->id}-%"
686 - ),
687 - OBJECT
688 - );
689 - }
664 + $items = $this->queue_storage->fetch_items( $limit, $order );
690 665
691 666 return $this->unserialize_values( $items );
692 667 }
693 668
@@ -698,28 +673,9 @@
698 673 *
699 674 * @return array|object|null
700 675 */
701 676 private function fetch_items_by_id( $items_ids ) {
702 - global $wpdb;
703 -
704 - // return early if $items_ids is empty or not an array.
705 - if ( empty( $items_ids ) || ! is_array( $items_ids ) ) {
706 - return null;
707 - }
708 -
709 - $ids_placeholders = implode( ', ', array_fill( 0, count( $items_ids ), '%s' ) );
710 - $query_with_placeholders = "SELECT option_name AS id, option_value AS value
711 - FROM $wpdb->options
712 - WHERE option_name IN ( $ids_placeholders )";
713 - $items = $wpdb->get_results(
714 - $wpdb->prepare(
715 - $query_with_placeholders, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
716 - $items_ids
717 - ),
718 - OBJECT
719 - );
720 -
721 - return $this->unserialize_values( $items );
677 + return $this->unserialize_values( $this->queue_storage->fetch_items_by_ids( $items_ids ) );
722 678 }
723 679
724 680 /**
725 681 * Unserialize item values.
@@ -743,9 +699,9 @@
743 699
744 700 /**
745 701 * Return true if the buffer is still valid or an Error other wise.
746 702 *
747 - * @param Automattic\Jetpack\Sync\Queue_Buffer $buffer The Queue_Buffer.
703 + * @param \Automattic\Jetpack\Sync\Queue_Buffer $buffer The Queue_Buffer.
748 704 *
749 705 * @return bool|WP_Error
750 706 */
751 707 private function validate_checkout( $buffer ) {