PluginProbe
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster / trunk
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster vtrunk
2.6.0 trunk 1.1.0 1.1.4 1.2.1 1.2.2 1.2.3 1.2.5 1.2.9 1.3.1 1.3.2 1.5.0 1.5.1 1.5.4 1.5.7 1.5.8 1.5.9 1.6.2 1.6.5 1.6.8 1.7.2 1.7.4 1.7.5 1.7.9 1.8.1 All 42 releases
sellkit / includes / core / libraries / wp-background-process.php

wp-background-process.php in SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster trunk, at includes/core/libraries/wp-background-process.php

518 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:ignoreFile
3
4 /**
5 * WP Background Process
6 *
7 * Multisite: queue batches and process locks use per-blog options/transients (not network
8 * sitemeta). SellKit tables live in each site's $wpdb->prefix; DB version is per-site in the
9 * sellkit option — a network-wide queue caused cross-site contention and duplicate migrations.
10 *
11 * @package WP-Background-Processing
12 */
13
14 defined( 'ABSPATH' ) || die();
15
16 if ( ! class_exists( 'WP_Background_Process' ) ) :
17
18 /**
19 * Abstract WP_Background_Process class.
20 *
21 * @abstract
22 * @extends WP_Async_Request
23 * @SuppressWarnings(PHPMD)
24 */
25 abstract class WP_Background_Process extends WP_Async_Request {
26
27 /**
28 * Action
29 *
30 * (default value: 'background_process')
31 *
32 * @var string
33 * @access protected
34 */
35 protected $action = 'background_process';
36
37 /**
38 * Start time of current process.
39 *
40 * (default value: 0)
41 *
42 * @var int
43 * @access protected
44 */
45 protected $start_time = 0;
46
47 /**
48 * Cron_hook_identifier
49 *
50 * @var mixed
51 * @access protected
52 */
53 protected $cron_hook_identifier;
54
55 /**
56 * Cron_interval_identifier
57 *
58 * @var mixed
59 * @access protected
60 */
61 protected $cron_interval_identifier;
62
63 /**
64 * Initiate new background process
65 */
66 public function __construct() {
67 parent::__construct();
68
69 $this->cron_hook_identifier = $this->identifier . '_cron';
70 $this->cron_interval_identifier = $this->identifier . '_cron_interval';
71
72 add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) );
73 add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) );
74 }
75
76 /**
77 * Dispatch
78 *
79 * @access public
80 * @return void
81 */
82 public function dispatch() {
83 // Schedule the cron healthcheck.
84 $this->schedule_event();
85
86 // Perform remote post.
87 return parent::dispatch();
88 }
89
90 /**
91 * Push to queue
92 *
93 * @param mixed $data Data.
94 *
95 * @return $this
96 */
97 public function push_to_queue( $data ) {
98 $this->data[] = $data;
99
100 return $this;
101 }
102
103 /**
104 * Save queue
105 *
106 * @return $this
107 */
108 public function save() {
109 $key = $this->generate_key();
110
111 if ( ! empty( $this->data ) ) {
112 update_option( $key, $this->data, false );
113 }
114
115 return $this;
116 }
117
118 /**
119 * Update queue
120 *
121 * @param string $key Key.
122 * @param array $data Data.
123 *
124 * @return $this
125 */
126 public function update( $key, $data ) {
127 if ( ! empty( $data ) ) {
128 update_option( $key, $data, false );
129 }
130
131 return $this;
132 }
133
134 /**
135 * Delete queue
136 *
137 * @param string $key Key.
138 *
139 * @return $this
140 */
141 public function delete( $key ) {
142 delete_option( $key );
143
144 return $this;
145 }
146
147 /**
148 * Generate key
149 *
150 * Generates a unique key based on microtime. Queue items are
151 * given a unique key so that they can be merged upon save.
152 *
153 * @param int $length Length.
154 *
155 * @return string
156 */
157 protected function generate_key( $length = 64 ) {
158 $unique = md5( microtime() . rand() );
159 $prepend = $this->identifier . '_batch_';
160
161 return substr( $prepend . $unique, 0, $length );
162 }
163
164 /**
165 * Maybe process queue
166 *
167 * Checks whether data exists within the queue and that
168 * the process is not already running.
169 */
170 public function maybe_handle() {
171 // Don't lock up other requests while processing
172 session_write_close();
173
174 if ( $this->is_process_running() ) {
175 // Background process already running.
176 wp_die();
177 }
178
179 if ( $this->is_queue_empty() ) {
180 // No data to process.
181 wp_die();
182 }
183
184 check_ajax_referer( $this->identifier, 'nonce' );
185
186 $this->handle();
187
188 wp_die();
189 }
190
191 /**
192 * Is queue empty
193 *
194 * @return bool
195 */
196 protected function is_queue_empty() {
197 global $wpdb;
198
199 $table = $wpdb->options;
200 $column = 'option_name';
201
202 $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
203
204 $count = $wpdb->get_var( $wpdb->prepare( "
205 SELECT COUNT(*)
206 FROM {$table}
207 WHERE {$column} LIKE %s
208 ", $key ) );
209
210 return ( $count > 0 ) ? false : true;
211 }
212
213 /**
214 * Is process running
215 *
216 * Check whether the current process is already running
217 * in a background process.
218 */
219 protected function is_process_running() {
220 if ( get_transient( $this->identifier . '_process_lock' ) ) {
221 // Process already running.
222 return true;
223 }
224
225 return false;
226 }
227
228 /**
229 * Lock process
230 *
231 * Lock the process so that multiple instances can't run simultaneously.
232 * Override if applicable, but the duration should be greater than that
233 * defined in the time_exceeded() method.
234 */
235 protected function lock_process() {
236 $this->start_time = time(); // Set start time of current process.
237
238 $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute
239 $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration );
240
241 set_transient( $this->identifier . '_process_lock', microtime(), $lock_duration );
242 }
243
244 /**
245 * Unlock process
246 *
247 * Unlock the process so that other instances can spawn.
248 *
249 * @return $this
250 */
251 protected function unlock_process() {
252 delete_transient( $this->identifier . '_process_lock' );
253
254 return $this;
255 }
256
257 /**
258 * Get batch
259 *
260 * @return stdClass Return the first batch from the queue
261 */
262 protected function get_batch() {
263 global $wpdb;
264
265 $table = $wpdb->options;
266 $column = 'option_name';
267 $key_column = 'option_id';
268 $value_column = 'option_value';
269
270 $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
271
272 $query = $wpdb->get_row( $wpdb->prepare( "
273 SELECT *
274 FROM {$table}
275 WHERE {$column} LIKE %s
276 ORDER BY {$key_column} ASC
277 LIMIT 1
278 ", $key ) );
279
280 $batch = new stdClass();
281
282 if ( ! $query ) {
283 $batch->key = '';
284 $batch->data = array();
285 return $batch;
286 }
287
288 $batch->key = $query->$column;
289 $batch->data = maybe_unserialize( $query->$value_column );
290
291 return $batch;
292 }
293
294 /**
295 * Handle
296 *
297 * Pass each queue item to the task handler, while remaining
298 * within server memory and time limit constraints.
299 */
300 protected function handle() {
301 $this->lock_process();
302
303 do {
304 $batch = $this->get_batch();
305
306 if ( empty( $batch->key ) ) {
307 break;
308 }
309
310 foreach ( $batch->data as $key => $value ) {
311 $task = $this->task( $value );
312
313 if ( false !== $task ) {
314 $batch->data[ $key ] = $task;
315 } else {
316 unset( $batch->data[ $key ] );
317 }
318
319 if ( $this->time_exceeded() || $this->memory_exceeded() ) {
320 // Batch limits reached.
321 break;
322 }
323 }
324
325 // Update or delete current batch.
326 if ( ! empty( $batch->data ) ) {
327 $this->update( $batch->key, $batch->data );
328 } else {
329 $this->delete( $batch->key );
330 }
331 } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() );
332
333 $this->unlock_process();
334
335 // Start next batch or complete process.
336 if ( ! $this->is_queue_empty() ) {
337 $this->dispatch();
338 } else {
339 $this->complete();
340 }
341
342 wp_die();
343 }
344
345 /**
346 * Memory exceeded
347 *
348 * Ensures the batch process never exceeds 90%
349 * of the maximum WordPress memory.
350 *
351 * @return bool
352 */
353 protected function memory_exceeded() {
354 $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory
355 $current_memory = memory_get_usage( true );
356 $return = false;
357
358 if ( $current_memory >= $memory_limit ) {
359 $return = true;
360 }
361
362 return apply_filters( $this->identifier . '_memory_exceeded', $return );
363 }
364
365 /**
366 * Get memory limit
367 *
368 * @return int
369 */
370 protected function get_memory_limit() {
371 if ( function_exists( 'ini_get' ) ) {
372 $memory_limit = ini_get( 'memory_limit' );
373 } else {
374 // Sensible default.
375 $memory_limit = '128M';
376 }
377
378 if ( ! $memory_limit || - 1 === intval( $memory_limit ) ) {
379 // Unlimited, set to 32GB.
380 $memory_limit = '32000M';
381 }
382
383 return wp_convert_hr_to_bytes( $memory_limit );
384 }
385
386 /**
387 * Time exceeded.
388 *
389 * Ensures the batch never exceeds a sensible time limit.
390 * A timeout limit of 30s is common on shared hosting.
391 *
392 * @return bool
393 */
394 protected function time_exceeded() {
395 $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds
396 $return = false;
397
398 if ( time() >= $finish ) {
399 $return = true;
400 }
401
402 return apply_filters( $this->identifier . '_time_exceeded', $return );
403 }
404
405 /**
406 * Complete.
407 *
408 * Override if applicable, but ensure that the below actions are
409 * performed, or, call parent::complete().
410 */
411 protected function complete() {
412 // Unschedule the cron healthcheck.
413 $this->clear_scheduled_event();
414 }
415
416 /**
417 * Schedule cron healthcheck
418 *
419 * @access public
420 *
421 * @param mixed $schedules Schedules.
422 *
423 * @return mixed
424 */
425 public function schedule_cron_healthcheck( $schedules ) {
426 $interval = apply_filters( $this->identifier . '_cron_interval', 5 );
427
428 if ( property_exists( $this, 'cron_interval' ) ) {
429 $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval );
430 }
431
432 // Adds every 5 minutes to the existing schedules.
433 $schedules[ $this->identifier . '_cron_interval' ] = array(
434 'interval' => MINUTE_IN_SECONDS * $interval,
435 'display' => sprintf( __( 'Every %d Minutes' ), $interval ),
436 );
437
438 return $schedules;
439 }
440
441 /**
442 * Handle cron healthcheck
443 *
444 * Restart the background process if not already running
445 * and data exists in the queue.
446 */
447 public function handle_cron_healthcheck() {
448 if ( $this->is_process_running() ) {
449 // Background process already running.
450 exit;
451 }
452
453 if ( $this->is_queue_empty() ) {
454 // No data to process.
455 $this->clear_scheduled_event();
456 exit;
457 }
458
459 $this->handle();
460
461 exit;
462 }
463
464 /**
465 * Schedule event
466 */
467 protected function schedule_event() {
468 if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) {
469 wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier );
470 }
471 }
472
473 /**
474 * Clear scheduled event
475 */
476 protected function clear_scheduled_event() {
477 $timestamp = wp_next_scheduled( $this->cron_hook_identifier );
478
479 if ( $timestamp ) {
480 wp_unschedule_event( $timestamp, $this->cron_hook_identifier );
481 }
482 }
483
484 /**
485 * Cancel Process
486 *
487 * Stop processing queue items, clear cronjob and delete batch.
488 *
489 */
490 public function cancel_process() {
491 if ( ! $this->is_queue_empty() ) {
492 $batch = $this->get_batch();
493
494 $this->delete( $batch->key );
495
496 wp_clear_scheduled_hook( $this->cron_hook_identifier );
497 }
498
499 }
500
501 /**
502 * Task
503 *
504 * Override this method to perform any actions required on each
505 * queue item. Return the modified item for further processing
506 * in the next pass through. Or, return false to remove the
507 * item from the queue.
508 *
509 * @param mixed $item Queue item to iterate over.
510 *
511 * @return mixed
512 */
513 abstract protected function task( $item );
514
515 }
516
517 endif;
518