PluginProbe
Ultimate WP Mail / trunk
Ultimate WP Mail vtrunk
1.3.13 1.3.12 trunk 0.11 0.25 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 All 46 releases
ultimate-wp-mail / lib / wp-background-processing / wp-background-process.php

wp-background-process.php in Ultimate WP Mail trunk, at lib/wp-background-processing/wp-background-process.php

734 lines 15.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WP Background Process
4 *
5 * @package WP-Background-Processing
6 */
7
8 /**
9 * Abstract WP_Background_Process class.
10 *
11 * @abstract
12 * @extends WP_Async_Request
13 */
14 abstract class EWD_WP_Background_Process extends EWD_WP_Async_Request {
15
16 /**
17 * Action
18 *
19 * (default value: 'background_process')
20 *
21 * @var string
22 * @access protected
23 */
24 protected $action = 'ewd_uwpm_background_process';
25
26 /**
27 * Start time of current process.
28 *
29 * (default value: 0)
30 *
31 * @var int
32 * @access protected
33 */
34 protected $start_time = 0;
35
36 /**
37 * Cron_hook_identifier
38 *
39 * @var string
40 * @access protected
41 */
42 protected $cron_hook_identifier;
43
44 /**
45 * Cron_interval_identifier
46 *
47 * @var string
48 * @access protected
49 */
50 protected $cron_interval_identifier;
51
52 /**
53 * The status set when process is cancelling.
54 *
55 * @var int
56 */
57 const STATUS_CANCELLED = 1;
58
59 /**
60 * The status set when process is paused or pausing.
61 *
62 * @var int;
63 */
64 const STATUS_PAUSED = 2;
65
66 /**
67 * Initiate new background process.
68 */
69 public function __construct() {
70 parent::__construct();
71
72 $this->cron_hook_identifier = $this->identifier . '_cron';
73 $this->cron_interval_identifier = $this->identifier . '_cron_interval';
74
75 add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) );
76 add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) );
77 }
78
79 /**
80 * Schedule the cron healthcheck and dispatch an async request to start processing the queue.
81 *
82 * @access public
83 * @return array|WP_Error|false HTTP Response array, WP_Error on failure, or false if not attempted.
84 */
85 public function dispatch() {
86 if ( $this->is_processing() ) {
87 // Process already running.
88 return false;
89 }
90
91 // Schedule the cron healthcheck.
92 $this->schedule_event();
93
94 // Perform remote post.
95 return parent::dispatch();
96 }
97
98 /**
99 * Push to the queue.
100 *
101 * Note, save must be called in order to persist queued items to a batch for processing.
102 *
103 * @param mixed $data Data.
104 *
105 * @return $this
106 */
107 public function push_to_queue( $data ) {
108 $this->data[] = $data;
109
110 return $this;
111 }
112
113 /**
114 * Save the queued items for future processing.
115 *
116 * @return $this
117 */
118 public function save() {
119 $key = $this->generate_key();
120
121 if ( ! empty( $this->data ) ) {
122 update_site_option( $key, $this->data );
123 }
124
125 // Clean out data so that new data isn't prepended with closed session's data.
126 $this->data = array();
127
128 return $this;
129 }
130
131 /**
132 * Update a batch's queued items.
133 *
134 * @param string $key Key.
135 * @param array $data Data.
136 *
137 * @return $this
138 */
139 public function update( $key, $data ) {
140 if ( ! empty( $data ) ) {
141 update_site_option( $key, $data );
142 }
143
144 return $this;
145 }
146
147 /**
148 * Delete a batch of queued items.
149 *
150 * @param string $key Key.
151 *
152 * @return $this
153 */
154 public function delete( $key ) {
155 delete_site_option( $key );
156
157 return $this;
158 }
159
160 /**
161 * Delete entire job queue.
162 */
163 public function delete_all() {
164 $batches = $this->get_batches();
165
166 foreach ( $batches as $batch ) {
167 $this->delete( $batch->key );
168 }
169
170 delete_site_option( $this->get_status_key() );
171
172 $this->cancelled();
173 }
174
175 /**
176 * Cancel job on next batch.
177 */
178 public function cancel() {
179 update_site_option( $this->get_status_key(), self::STATUS_CANCELLED );
180
181 // Just in case the job was paused at the time.
182 $this->dispatch();
183 }
184
185 /**
186 * Has the process been cancelled?
187 *
188 * @return bool
189 */
190 public function is_cancelled() {
191 $status = get_site_option( $this->get_status_key(), 0 );
192
193 if ( absint( $status ) === self::STATUS_CANCELLED ) {
194 return true;
195 }
196
197 return false;
198 }
199
200 /**
201 * Called when background process has been cancelled.
202 */
203 protected function cancelled() {
204 do_action( $this->identifier . '_cancelled' );
205 }
206
207 /**
208 * Pause job on next batch.
209 */
210 public function pause() {
211 update_site_option( $this->get_status_key(), self::STATUS_PAUSED );
212 }
213
214 /**
215 * Is the job paused?
216 *
217 * @return bool
218 */
219 public function is_paused() {
220 $status = get_site_option( $this->get_status_key(), 0 );
221
222 if ( absint( $status ) === self::STATUS_PAUSED ) {
223 return true;
224 }
225
226 return false;
227 }
228
229 /**
230 * Called when background process has been paused.
231 */
232 protected function paused() {
233 do_action( $this->identifier . '_paused' );
234 }
235
236 /**
237 * Resume job.
238 */
239 public function resume() {
240 delete_site_option( $this->get_status_key() );
241
242 $this->schedule_event();
243 $this->dispatch();
244 $this->resumed();
245 }
246
247 /**
248 * Called when background process has been resumed.
249 */
250 protected function resumed() {
251 do_action( $this->identifier . '_resumed' );
252 }
253
254 /**
255 * Is queued?
256 *
257 * @return bool
258 */
259 public function is_queued() {
260 return ! $this->is_queue_empty();
261 }
262
263 /**
264 * Is the tool currently active, e.g. starting, working, paused or cleaning up?
265 *
266 * @return bool
267 */
268 public function is_active() {
269 return $this->is_queued() || $this->is_processing() || $this->is_paused() || $this->is_cancelled();
270 }
271
272 /**
273 * Generate key for a batch.
274 *
275 * Generates a unique key based on microtime. Queue items are
276 * given a unique key so that they can be merged upon save.
277 *
278 * @param int $length Optional max length to trim key to, defaults to 64 characters.
279 * @param string $key Optional string to append to identifier before hash, defaults to "batch".
280 *
281 * @return string
282 */
283 protected function generate_key( $length = 64, $key = 'batch' ) {
284 $unique = md5( microtime() . wp_rand() );
285 $prepend = $this->identifier . '_' . $key . '_';
286
287 return substr( $prepend . $unique, 0, $length );
288 }
289
290 /**
291 * Get the status key.
292 *
293 * @return string
294 */
295 protected function get_status_key() {
296 return $this->identifier . '_status';
297 }
298
299 /**
300 * Maybe process a batch of queued items.
301 *
302 * Checks whether data exists within the queue and that
303 * the process is not already running.
304 */
305 public function maybe_handle() {
306 // Don't lock up other requests while processing.
307 session_write_close();
308
309 if ( $this->is_processing() ) {
310 // Background process already running.
311 return $this->maybe_wp_die();
312 }
313
314 if ( $this->is_cancelled() ) {
315 $this->clear_scheduled_event();
316 $this->delete_all();
317
318 return $this->maybe_wp_die();
319 }
320
321 if ( $this->is_paused() ) {
322 $this->clear_scheduled_event();
323 $this->paused();
324
325 return $this->maybe_wp_die();
326 }
327
328 if ( $this->is_queue_empty() ) {
329 // No data to process.
330 return $this->maybe_wp_die();
331 }
332
333 check_ajax_referer( $this->identifier, 'nonce' );
334
335 $this->handle();
336
337 return $this->maybe_wp_die();
338 }
339
340 /**
341 * Is queue empty?
342 *
343 * @return bool
344 */
345 protected function is_queue_empty() {
346 return empty( $this->get_batch() );
347 }
348
349 /**
350 * Is process running?
351 *
352 * Check whether the current process is already running
353 * in a background process.
354 *
355 * @return bool
356 *
357 * @deprecated 1.1.0 Superseded.
358 * @see is_processing()
359 */
360 protected function is_process_running() {
361 return $this->is_processing();
362 }
363
364 /**
365 * Is the background process currently running?
366 *
367 * @return bool
368 */
369 public function is_processing() {
370 if ( get_site_transient( $this->identifier . '_process_lock' ) ) {
371 // Process already running.
372 return true;
373 }
374
375 return false;
376 }
377
378 /**
379 * Lock process.
380 *
381 * Lock the process so that multiple instances can't run simultaneously.
382 * Override if applicable, but the duration should be greater than that
383 * defined in the time_exceeded() method.
384 */
385 protected function lock_process() {
386 $this->start_time = time(); // Set start time of current process.
387
388 $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute
389 $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration );
390
391 set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration );
392 }
393
394 /**
395 * Unlock process.
396 *
397 * Unlock the process so that other instances can spawn.
398 *
399 * @return $this
400 */
401 protected function unlock_process() {
402 delete_site_transient( $this->identifier . '_process_lock' );
403
404 return $this;
405 }
406
407 /**
408 * Get batch.
409 *
410 * @return stdClass Return the first batch of queued items.
411 */
412 protected function get_batch() {
413 return array_reduce(
414 $this->get_batches( 1 ),
415 function ( $carry, $batch ) {
416 return $batch;
417 },
418 array()
419 );
420 }
421
422 /**
423 * Get batches.
424 *
425 * @param int $limit Number of batches to return, defaults to all.
426 *
427 * @return array of stdClass
428 */
429 public function get_batches( $limit = 0 ) {
430 global $wpdb;
431
432 if ( empty( $limit ) || ! is_int( $limit ) ) {
433 $limit = 0;
434 }
435
436 $table = $wpdb->options;
437 $column = 'option_name';
438 $key_column = 'option_id';
439 $value_column = 'option_value';
440
441 if ( is_multisite() ) {
442 $table = $wpdb->sitemeta;
443 $column = 'meta_key';
444 $key_column = 'meta_id';
445 $value_column = 'meta_value';
446 }
447
448 $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
449
450 $sql = '
451 SELECT *
452 FROM ' . $table . '
453 WHERE ' . $column . ' LIKE %s
454 ORDER BY ' . $key_column . ' ASC
455 ';
456
457 $args = array( $key );
458
459 if ( ! empty( $limit ) ) {
460 $sql .= ' LIMIT %d';
461
462 $args[] = $limit;
463 }
464
465 $items = $wpdb->get_results( $wpdb->prepare( $sql, $args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
466
467 $batches = array();
468
469 if ( ! empty( $items ) ) {
470 $batches = array_map(
471 function ( $item ) use ( $column, $value_column ) {
472 $batch = new stdClass();
473 $batch->key = $item->{$column};
474 $batch->data = maybe_unserialize( $item->{$value_column} );
475
476 return $batch;
477 },
478 $items
479 );
480 }
481
482 return $batches;
483 }
484
485 /**
486 * Handle a dispatched request.
487 *
488 * Pass each queue item to the task handler, while remaining
489 * within server memory and time limit constraints.
490 */
491 protected function handle() {
492 $this->lock_process();
493
494 /**
495 * Number of seconds to sleep between batches. Defaults to 0 seconds, minimum 0.
496 *
497 * @param int $seconds
498 */
499 $throttle_seconds = max(
500 0,
501 apply_filters(
502 $this->identifier . '_seconds_between_batches',
503 apply_filters(
504 $this->prefix . '_seconds_between_batches',
505 0
506 )
507 )
508 );
509
510 do {
511 $batch = $this->get_batch();
512
513 foreach ( $batch->data as $key => $value ) {
514 $task = $this->task( $value );
515
516 if ( false !== $task ) {
517 $batch->data[ $key ] = $task;
518 } else {
519 unset( $batch->data[ $key ] );
520 }
521
522 // Keep the batch up to date while processing it.
523 if ( ! empty( $batch->data ) ) {
524 $this->update( $batch->key, $batch->data );
525 }
526
527 // Let the server breathe a little.
528 sleep( $throttle_seconds );
529
530 if ( $this->time_exceeded() || $this->memory_exceeded() ) {
531 // Batch limits reached.
532 break;
533 }
534 }
535
536 // Delete current batch if fully processed.
537 if ( empty( $batch->data ) ) {
538 $this->delete( $batch->key );
539 }
540 } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() );
541
542 $this->unlock_process();
543
544 // Start next batch or complete process.
545 if ( ! $this->is_queue_empty() ) {
546 $this->dispatch();
547 } else {
548 $this->complete();
549 }
550
551 return $this->maybe_wp_die();
552 }
553
554 /**
555 * Memory exceeded?
556 *
557 * Ensures the batch process never exceeds 90%
558 * of the maximum WordPress memory.
559 *
560 * @return bool
561 */
562 protected function memory_exceeded() {
563 $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory
564 $current_memory = memory_get_usage( true );
565 $return = false;
566
567 if ( $current_memory >= $memory_limit ) {
568 $return = true;
569 }
570
571 return apply_filters( $this->identifier . '_memory_exceeded', $return );
572 }
573
574 /**
575 * Get memory limit in bytes.
576 *
577 * @return int
578 */
579 protected function get_memory_limit() {
580 if ( function_exists( 'ini_get' ) ) {
581 $memory_limit = ini_get( 'memory_limit' );
582 } else {
583 // Sensible default.
584 $memory_limit = '128M';
585 }
586
587 if ( ! $memory_limit || -1 === intval( $memory_limit ) ) {
588 // Unlimited, set to 32GB.
589 $memory_limit = '32000M';
590 }
591
592 return wp_convert_hr_to_bytes( $memory_limit );
593 }
594
595 /**
596 * Time limit exceeded?
597 *
598 * Ensures the batch never exceeds a sensible time limit.
599 * A timeout limit of 30s is common on shared hosting.
600 *
601 * @return bool
602 */
603 protected function time_exceeded() {
604 $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds
605 $return = false;
606
607 if ( time() >= $finish ) {
608 $return = true;
609 }
610
611 return apply_filters( $this->identifier . '_time_exceeded', $return );
612 }
613
614 /**
615 * Complete processing.
616 *
617 * Override if applicable, but ensure that the below actions are
618 * performed, or, call parent::complete().
619 */
620 protected function complete() {
621 delete_site_option( $this->get_status_key() );
622
623 // Remove the cron healthcheck job from the cron schedule.
624 $this->clear_scheduled_event();
625
626 $this->completed();
627 }
628
629 /**
630 * Called when background process has completed.
631 */
632 protected function completed() {
633 do_action( $this->identifier . '_completed' );
634 }
635
636 /**
637 * Schedule the cron healthcheck job.
638 *
639 * @access public
640 *
641 * @param mixed $schedules Schedules.
642 *
643 * @return mixed
644 */
645 public function schedule_cron_healthcheck( $schedules ) {
646 $interval = apply_filters( $this->cron_interval_identifier, 5 );
647
648 if ( property_exists( $this, 'cron_interval' ) ) {
649 $interval = apply_filters( $this->cron_interval_identifier, $this->cron_interval );
650 }
651
652 if ( 1 === $interval ) {
653 $display = __( 'Every Minute' );
654 } else {
655 $display = sprintf( __( 'Every %d Minutes' ), $interval );
656 }
657
658 // Adds an "Every NNN Minute(s)" schedule to the existing cron schedules.
659 $schedules[ $this->cron_interval_identifier ] = array(
660 'interval' => MINUTE_IN_SECONDS * $interval,
661 'display' => $display,
662 );
663
664 return $schedules;
665 }
666
667 /**
668 * Handle cron healthcheck event.
669 *
670 * Restart the background process if not already running
671 * and data exists in the queue.
672 */
673 public function handle_cron_healthcheck() {
674 if ( $this->is_processing() ) {
675 // Background process already running.
676 exit;
677 }
678
679 if ( $this->is_queue_empty() ) {
680 // No data to process.
681 $this->clear_scheduled_event();
682 exit;
683 }
684
685 $this->dispatch();
686 }
687
688 /**
689 * Schedule the cron healthcheck event.
690 */
691 protected function schedule_event() {
692 if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) {
693 wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier );
694 }
695 }
696
697 /**
698 * Clear scheduled cron healthcheck event.
699 */
700 protected function clear_scheduled_event() {
701 $timestamp = wp_next_scheduled( $this->cron_hook_identifier );
702
703 if ( $timestamp ) {
704 wp_unschedule_event( $timestamp, $this->cron_hook_identifier );
705 }
706 }
707
708 /**
709 * Cancel the background process.
710 *
711 * Stop processing queue items, clear cron job and delete batch.
712 *
713 * @deprecated 1.1.0 Superseded.
714 * @see cancel()
715 */
716 public function cancel_process() {
717 $this->cancel();
718 }
719
720 /**
721 * Perform task with queued item.
722 *
723 * Override this method to perform any actions required on each
724 * queue item. Return the modified item for further processing
725 * in the next pass through. Or, return false to remove the
726 * item from the queue.
727 *
728 * @param mixed $item Queue item to iterate over.
729 *
730 * @return mixed
731 */
732 abstract protected function task( $item );
733 }
734