PluginProbe ʕ •ᴥ•ʔ
WooCommerce Square / 5.4.2
WooCommerce Square v5.4.2
5.4.2 5.4.1 5.4.0 trunk 1.0.25 1.0.26 1.0.27 1.0.28 1.0.29 1.0.30 1.0.31 1.0.32 1.0.33 1.0.34 1.0.35 1.0.36 1.0.37 1.0.38 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.6.0 2.7.0 2.8.0 2.9.0 2.9.1 3.0.0 3.0.1 3.0.2 3.0.3 3.1.0 3.2.0 3.3.0 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.7.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 4.0.0 4.1.0 4.2.0 4.2.1 4.2.2 4.2.3 4.3.0 4.3.1 4.3.2 4.4.0 4.4.1 4.4.2 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.6.2 4.6.3 4.6.4 4.7.0 4.7.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.1 4.8.2 4.8.3 4.8.4 4.8.5 4.8.6 4.8.7 4.8.8 4.9.0 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 4.9.6 4.9.7 4.9.8 4.9.9 5.0.0 5.0.1 5.1.0 5.1.1 5.1.2 5.2.0 5.3.0 5.3.1 5.3.2 5.3.3
woocommerce-square / includes / Framework / Utilities / WP_Background_Job_Handler.php
woocommerce-square / includes / Framework / Utilities Last commit date
WP_Background_Job_Handler.php 1 year ago
WP_Background_Job_Handler.php
778 lines
1 <?php
2 /**
3 * WooCommerce Plugin Framework
4 *
5 * This source file is subject to the GNU General Public License v3.0
6 * that is bundled with this package in the file license.txt.
7 * It is also available through the world-wide-web at this URL:
8 * http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later
9 * If you did not receive a copy of the license and are unable to
10 * obtain it through the world-wide-web, please send an email
11 * to license@skyverge.com so we can send you a copy immediately.
12 *
13 * @since 3.0.0
14 * @author WooCommerce / SkyVerge / Delicious Brains
15 * @copyright Copyright (c) 2015-2016 Delicious Brains Inc.
16 * @copyright Copyright (c) 2013-2019, SkyVerge, Inc.
17 * @license http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later
18 *
19 * Modified by WooCommerce on 01 December 2021.
20 */
21
22 namespace WooCommerce\Square\Framework\Utilities;
23
24 use WooCommerce\Square\Framework\Plugin_Compatibility;
25
26 defined( 'ABSPATH' ) or exit;
27
28 /**
29 * Square WordPress Background Job Handler class
30 *
31 * Based on the wonderful WP_Background_Process class by deliciousbrains:
32 * https://github.com/A5hleyRich/wp-background-processing
33 *
34 * Subclasses SV_WP_Async_Request. Instead of the concept of `batches` used in
35 * the Delicious Brains' version, however, this takes a more object-oriented approach
36 * of background `jobs`, allowing greater control over manipulating job data and
37 * processing.
38 *
39 * A batch implicitly expected an array of items to process, whereas a job does
40 * not expect any particular data structure (although it does default to
41 * looping over job data) and allows subclasses to provide their own
42 * processing logic.
43 *
44 * @since 3.0.0
45 */
46 abstract class Background_Job_Handler {
47
48
49 /** @var string async request prefix */
50 protected $prefix = 'sv_wp';
51
52 /** @var string async request action */
53 protected $action = 'background_job';
54
55 /** @var string data key */
56 protected $data_key = 'data';
57
58 /** @var int start time of current process */
59 protected $start_time = 0;
60
61 /** @var string cron hook identifier */
62 protected $cron_hook_identifier;
63
64 /** @var string cron interval identifier */
65 protected $cron_interval_identifier;
66
67 /** @var string debug message, used by the system status tool */
68 protected $debug_message;
69
70 /** @var string job identifier */
71 protected $identifier;
72
73
74 /**
75 * Initiate new background job handler
76 *
77 * @since 3.0.0
78 */
79 public function __construct() {
80
81 $this->identifier = $this->prefix . '_' . $this->action;
82 $this->cron_hook_identifier = $this->identifier . '_cron';
83 $this->cron_interval_identifier = $this->identifier . '_cron_interval';
84
85 $this->add_hooks();
86 }
87
88
89 /**
90 * Adds the necessary action and filter hooks.
91 *
92 * @since 3.0.0
93 */
94 protected function add_hooks() {
95
96 // cron healthcheck
97 add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) );
98
99 // debugging & testing
100 add_filter( 'gettext', array( $this, 'translate_success_message' ), 10, 3 );
101 }
102
103 /**
104 * Check whether job queue is empty or not
105 *
106 * @since 3.0.0
107 * @return bool True if queue is empty, false otherwise
108 */
109 protected function is_queue_empty() {
110 global $wpdb;
111
112 $key = $this->identifier . '_job_%';
113
114 // only queued or processing jobs count
115 $queued = '%"status":"queued"%';
116 $processing = '%"status":"processing"%';
117
118 $count = $wpdb->get_var( $wpdb->prepare( "
119 SELECT COUNT(*)
120 FROM {$wpdb->options}
121 WHERE option_name LIKE %s
122 AND ( option_value LIKE %s OR option_value LIKE %s )
123 ", $key, $queued, $processing ) );
124
125 return ( $count > 0 ) ? false : true;
126 }
127
128
129 /**
130 * Check whether background process is running or not
131 *
132 * Check whether the current process is already running
133 * in a background process.
134 *
135 * @since 3.0.0
136 * @return bool True if processing is running, false otherwise
137 */
138 protected function is_process_running() {
139
140 // add a random artificial delay to prevent a race condition if 2 or more processes are trying to
141 // process the job queue at the very same moment in time and neither of them have yet set the lock
142 // before the others are calling this method
143 usleep( wp_rand( 100000, 300000 ) );
144
145 return (bool) get_transient( "{$this->identifier}_process_lock" );
146 }
147
148
149 /**
150 * Lock process
151 *
152 * Lock the process so that multiple instances can't run simultaneously.
153 * Override if applicable, but the duration should be greater than that
154 * defined in the time_exceeded() method.
155 *
156 * @since 3.0.0
157 */
158 protected function lock_process() {
159
160 // set start time of current process
161 $this->start_time = time();
162
163 // set lock duration to 1 minute by default
164 $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60;
165
166 /**
167 * Filter the queue lock time
168 *
169 * @since 3.0.0
170 * @param int $lock_duration Lock duration in seconds
171 */
172 $lock_duration = apply_filters( "{$this->identifier}_queue_lock_time", $lock_duration );
173
174 set_transient( "{$this->identifier}_process_lock", microtime(), $lock_duration );
175 }
176
177
178 /**
179 * Unlock process
180 *
181 * Unlock the process so that other instances can spawn.
182 *
183 * @since 3.0.0
184 * @return Background_Job_Handler
185 */
186 protected function unlock_process() {
187
188 delete_transient( "{$this->identifier}_process_lock" );
189
190 return $this;
191 }
192
193
194 /**
195 * Check if memory limit is exceeded
196 *
197 * Ensures the background job handler process never exceeds 90%
198 * of the maximum WordPress memory.
199 *
200 * @since 3.0.0
201 *
202 * @return bool True if exceeded memory limit, false otherwise
203 */
204 protected function memory_exceeded() {
205
206 $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory
207 $current_memory = memory_get_usage( true );
208 $return = false;
209
210 if ( $current_memory >= $memory_limit ) {
211 $return = true;
212 }
213
214 /**
215 * Filter whether memory limit has been exceeded or not
216 *
217 * @since 3.0.0
218 *
219 * @param bool $exceeded
220 */
221 return apply_filters( "{$this->identifier}_memory_exceeded", $return );
222 }
223
224
225 /**
226 * Get memory limit
227 *
228 * @since 3.0.0
229 *
230 * @return int memory limit in bytes
231 */
232 protected function get_memory_limit() {
233
234 if ( function_exists( 'ini_get' ) ) {
235 $memory_limit = ini_get( 'memory_limit' );
236 } else {
237 // sensible default
238 $memory_limit = '128M';
239 }
240
241 if ( ! $memory_limit || -1 === (int) $memory_limit ) {
242 // unlimited, set to 32GB
243 $memory_limit = '32G';
244 }
245
246 return Plugin_Compatibility::convert_hr_to_bytes( $memory_limit );
247 }
248
249 /**
250 * Create a background job
251 *
252 * Delicious Brains' versions alternative would be using ->data()->save().
253 * Allows passing in any kind of job attributes, which will be available at item data processing time.
254 * This allows sharing common options between items without the need to repeat
255 * the same information for every single item in queue.
256 *
257 * Instead of returning self, returns the job instance, which gives greater
258 * control over the job.
259 *
260 * @since 3.0.0
261 *
262 * @param array|mixed $attrs Job attributes.
263 * @return \stdClass|object|null
264 */
265 public function create_job( $attrs ) {
266 global $wpdb;
267
268 if ( empty( $attrs ) ) {
269 return null;
270 }
271
272 // generate a unique ID for the job
273 $job_id = md5( microtime() . wp_rand() );
274
275 /**
276 * Filter new background job attributes
277 *
278 * @since 3.0.0
279 *
280 * @param array $attrs Job attributes
281 * @param string $id Job ID
282 */
283 $attrs = apply_filters( "{$this->identifier}_new_job_attrs", $attrs, $job_id );
284
285 // ensure a few must-have attributes
286 $attrs = wp_parse_args( array(
287 'id' => $job_id,
288 'created_at' => current_time( 'mysql' ),
289 'created_by' => get_current_user_id(),
290 'status' => 'queued',
291 ), $attrs );
292
293 $wpdb->insert( $wpdb->options, array(
294 'option_name' => "{$this->identifier}_job_{$job_id}",
295 'option_value' => wp_json_encode( $attrs ),
296 'autoload' => 'no'
297 ) );
298
299 $job = new \stdClass();
300
301 foreach ( $attrs as $key => $value ) {
302 $job->{$key} = $value;
303 }
304
305 /**
306 * Runs when a job is created.
307 *
308 * @since 3.0.0
309 *
310 * @param \stdClass|object $job the created job
311 */
312 do_action( "{$this->identifier}_job_created", $job );
313
314 return $job;
315 }
316
317
318 /**
319 * Get a job (by default the first in the queue)
320 *
321 * @since 3.0.0
322 *
323 * @param string $id Optional. Job ID. Will return first job in queue if not
324 * provided. Will not return completed or failed jobs from queue.
325 * @return \stdClass|object|null The found job object or null
326 */
327 public function get_job( $id = null ) {
328 global $wpdb;
329
330 if ( ! $id ) {
331
332 $key = $this->identifier . '_job_%';
333 $queued = '%"status":"queued"%';
334 $processing = '%"status":"processing"%';
335
336 $results = $wpdb->get_var( $wpdb->prepare( "
337 SELECT option_value
338 FROM {$wpdb->options}
339 WHERE option_name LIKE %s
340 AND ( option_value LIKE %s OR option_value LIKE %s )
341 ORDER BY option_id ASC
342 LIMIT 1
343 ", $key, $queued, $processing ) );
344
345 } else {
346
347 $results = $wpdb->get_var( $wpdb->prepare( "
348 SELECT option_value
349 FROM {$wpdb->options}
350 WHERE option_name = %s
351 ", "{$this->identifier}_job_{$id}" ) );
352
353 }
354
355 if ( ! empty( $results ) ) {
356
357 $job = new \stdClass();
358
359 foreach ( json_decode( $results, true ) as $key => $value ) {
360 $job->{$key} = $value;
361 }
362
363 } else {
364 return null;
365 }
366
367 /**
368 * Filters the job as returned from the database.
369 *
370 * @since 3.0.0
371 *
372 * @param \stdClass|object $job
373 */
374 return apply_filters( "{$this->identifier}_returned_job", $job );
375 }
376
377
378 /**
379 * Gets jobs.
380 *
381 * @since 3.0.0
382 *
383 * @param array $args {
384 * Optional. An array of arguments
385 *
386 * @type string|array $status Job status(es) to include
387 * @type string $order ASC or DESC. Defaults to DESC
388 * @type string $orderby Field to order by. Defaults to option_id
389 * }
390 * @return \stdClass[]|object[]|null Found jobs or null if none found
391 */
392 public function get_jobs( $args = array() ) {
393 global $wpdb;
394
395 $args = wp_parse_args( $args, array(
396 'order' => 'DESC',
397 'orderby' => 'option_id',
398 ) );
399
400 $replacements = array( $this->identifier . '_job_%' );
401 $status_query = '';
402
403 // prepare status query
404 if ( ! empty( $args['status'] ) ) {
405
406 $statuses = (array) $args['status'];
407 $placeholders = array();
408
409 foreach ( $statuses as $status ) {
410
411 $placeholders[] = '%s';
412 $replacements[] = '%"status":"' . sanitize_key( $status ) . '"%';
413 }
414
415 $status_query = 'AND ( option_value LIKE ' . implode( ' OR option_value LIKE ', $placeholders ) . ' )';
416 }
417
418 // prepare sorting vars
419 $order = sanitize_key( $args['order'] );
420 $orderby = sanitize_key( $args['orderby'] );
421
422 // put it all together now
423 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Data is already sanitized to direct use.
424 $results = $wpdb->get_col(
425 $wpdb->prepare(
426 "SELECT option_value
427 FROM {$wpdb->options}
428 WHERE option_name LIKE %s
429 {$status_query}
430 ORDER BY {$orderby} {$order}",
431 $replacements
432 )
433 );
434 // phpcs:enable
435
436 if ( empty( $results ) ) {
437 return null;
438 }
439
440 $jobs = array();
441
442 foreach ( $results as $result ) {
443
444 $job = new \stdClass();
445
446 foreach ( json_decode( $result, true ) as $key => $value ) {
447 $job->{$key} = $value;
448 }
449
450 /* This filter is documented above. */
451 $job = apply_filters( "{$this->identifier}_returned_job", $job );
452
453 $jobs[] = $job;
454 }
455
456 return $jobs;
457 }
458
459 /**
460 * Update job attrs
461 *
462 * @since 3.0.0
463 *
464 * @param \stdClass|object|string $job Job instance or ID
465 * @return \stdClass|object|false on failure
466 */
467 public function update_job( $job ) {
468
469 if ( is_string( $job ) ) {
470 $job = $this->get_job( $job );
471 }
472
473 if ( ! $job ) {
474 return false;
475 }
476
477 $job->updated_at = current_time( 'mysql' );
478
479 $this->update_job_option( $job );
480
481 /**
482 * Runs when a job is updated.
483 *
484 * @since 3.0.0
485 *
486 * @param \stdClass|object $job the updated job
487 */
488 do_action( "{$this->identifier}_job_updated", $job );
489
490 return $job;
491 }
492
493
494 /**
495 * Handles job completion.
496 *
497 * @since 3.0.0
498 *
499 * @param \stdClass|object|string $job Job instance or ID
500 * @return \stdClass|object|false on failure
501 */
502 public function complete_job( $job ) {
503
504 if ( is_string( $job ) ) {
505 $job = $this->get_job( $job );
506 }
507
508 if ( ! $job ) {
509 return false;
510 }
511
512 $job->status = 'completed';
513 $job->completed_at = current_time( 'mysql' );
514
515 $this->update_job_option( $job );
516
517 /**
518 * Runs when a job is completed.
519 *
520 * @since 3.0.0
521 *
522 * @param \stdClass|object $job the completed job
523 */
524 do_action( "{$this->identifier}_job_complete", $job );
525
526 return $job;
527 }
528
529
530 /**
531 * Handle job failure
532 *
533 * Default implementation does not call this method directly, but it's
534 * provided as a convenience method for subclasses that may call this to
535 * indicate that a particular job has failed for some reason.
536 *
537 * @since 3.0.0
538 *
539 * @param \stdClass|object|string $job Job instance or ID
540 * @param string $reason Optional. Reason for failure.
541 * @return \stdClass|false on failure
542 */
543 public function fail_job( $job, $reason = '' ) {
544
545 if ( is_string( $job ) ) {
546 $job = $this->get_job( $job );
547 }
548
549 if ( ! $job ) {
550 return false;
551 }
552
553 $job->status = 'failed';
554 $job->failed_at = current_time( 'mysql' );
555
556 if ( $reason ) {
557 $job->failure_reason = $reason;
558 }
559
560 $this->update_job_option( $job );
561
562 /**
563 * Runs when a job is failed.
564 *
565 * @since 3.0.0
566 *
567 * @param \stdClass|object $job the failed job
568 */
569 do_action( "{$this->identifier}_job_failed", $job );
570
571 return $job;
572 }
573
574
575 /**
576 * Delete a job
577 *
578 * @since 3.0.0
579 *
580 * @param \stdClass|object|string $job Job instance or ID
581 * @return bool|void returns false on failure
582 */
583 public function delete_job( $job ) {
584 global $wpdb;
585
586 if ( is_string( $job ) ) {
587 $job = $this->get_job( $job );
588 }
589
590 if ( ! $job ) {
591 return false;
592 }
593
594 $wpdb->delete( $wpdb->options, array( 'option_name' => "{$this->identifier}_job_{$job->id}" ) );
595
596 /**
597 * Runs after a job is deleted.
598 *
599 * @since 3.0.0
600 *
601 * @param \stdClass|object $job the job that was deleted from database
602 */
603 do_action( "{$this->identifier}_job_deleted", $job );
604 }
605
606
607 /**
608 * Handle job queue completion
609 *
610 * Override if applicable, but ensure that the below actions are
611 * performed, or, call parent::complete().
612 *
613 * @since 3.0.0
614 */
615 protected function complete() {
616
617 // unschedule the cron healthcheck
618 $this->clear_scheduled_event();
619 }
620
621
622 /**
623 * Schedule cron healthcheck
624 *
625 * @since 3.0.0
626 * @param array $schedules
627 * @return array
628 */
629 public function schedule_cron_healthcheck( $schedules ) {
630
631 $interval = property_exists( $this, 'cron_interval' ) ? $this->cron_interval : 5;
632
633 /**
634 * Filter cron health check interval
635 *
636 * @since 3.0.0
637 * @param int $interval Interval in minutes
638 */
639 $interval = apply_filters( "{$this->identifier}_cron_interval", $interval );
640
641 // adds every 5 minutes to the existing schedules.
642 $schedules[ $this->identifier . '_cron_interval' ] = array(
643 'interval' => MINUTE_IN_SECONDS * $interval,
644 /* translators: %d: interval in minutes */
645 'display' => sprintf( esc_html__( 'Every %d Minutes', 'woocommerce-square' ), $interval ),
646 );
647
648 return $schedules;
649 }
650
651 /**
652 * Schedule cron health check event
653 *
654 * @since 3.0.0
655 */
656 protected function schedule_event() {
657 /**
658 * Filters the interval of sync healthcheck action.
659 *
660 * @since 3.8.2
661 *
662 * @param int $interval sync heathcheck interval in seconds (defaults to 5 minutes)
663 */
664 $interval = apply_filters( 'wc_square_sync_healthcheck_interval', 300 );
665 $group = 'square';
666
667 if ( false === as_next_scheduled_action( $this->cron_hook_identifier, array(), $group ) ) {
668 // Schedule the health check to fire after 5 mins from now, and run every 5 mins until queue get empty.
669 as_schedule_recurring_action( time() + $interval, $interval, $this->cron_hook_identifier, array(), $group );
670 }
671 }
672
673 /**
674 * Clear scheduled health check event
675 *
676 * @since 3.0.0
677 */
678 protected function clear_scheduled_event() {
679 as_unschedule_all_actions( $this->cron_hook_identifier );
680 }
681
682
683 /**
684 * Process an item from job data
685 *
686 * Implement this method to perform any actions required on each
687 * item in job data.
688 *
689 * @since 3.0.0
690 *
691 * @param mixed $item Job data item to iterate over
692 * @param \stdClass|object $job Job instance
693 * @return mixed
694 */
695 abstract protected function process_item( $item, $job );
696
697
698 /**
699 * Handles PHP shutdown, say after a fatal error.
700 *
701 * @since 3.0.0
702 *
703 * @param \stdClass|object $job the job being processed
704 */
705 public function handle_shutdown( $job ) {
706
707 $error = error_get_last();
708
709 // if shutting down because of a fatal error, fail the job
710 if ( $error && E_ERROR === $error['type'] ) {
711
712 $this->fail_job( $job, $error['message'] );
713
714 $this->unlock_process();
715 }
716 }
717
718
719 /**
720 * Update a job option in options database.
721 *
722 * @since 3.0.0
723 *
724 * @param \stdClass|object $job the job instance to update in database
725 * @return int|bool number of rows updated or false on failure, see wpdb::update()
726 */
727 private function update_job_option( $job ) {
728 global $wpdb;
729
730 return $wpdb->update(
731 $wpdb->options,
732 array( 'option_value' => wp_json_encode( $job ) ),
733 array( 'option_name' => "{$this->identifier}_job_{$job->id}" )
734 );
735 }
736
737
738 /** Debug & Testing Methods ***********************************************/
739
740 /**
741 * Translate the tool success message.
742 *
743 * This can be removed in favor of returning the message string in `run_debug_tool()`
744 * when WC 3.1 is required, though that means the message will always be "success" styled.
745 *
746 * @since 3.0.0
747 *
748 * @param string $translated the text to output
749 * @param string $original the original text
750 * @param string $domain the textdomain
751 * @return string the updated text
752 */
753 public function translate_success_message( $translated, $original, $domain ) {
754
755 if ( 'woocommerce' === $domain && ( 'Tool ran.' === $original || 'There was an error calling %s' === $original ) ) {
756 $translated = $this->debug_message;
757 }
758
759 return $translated;
760 }
761
762
763 /** Helper Methods ********************************************************/
764
765
766 /**
767 * Gets the job handler identifier.
768 *
769 * @since 3.0.0
770 *
771 * @return string
772 */
773 public function get_identifier() {
774
775 return $this->identifier;
776 }
777 }
778