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