✏️ 编辑:WP_Background_Job_Handler.php
路径:
/home/forge/woocommerce-square/includes/Framework/Utilities/WP_Background_Job_Handler.php
大小:18.1 KB · 修改:2025-12-23 20:26:26 · 权限:0644 · 可写
← 返回目录
👁 查看
⬇ 下载
<?php /** * WooCommerce Plugin Framework * * This source file is subject to the GNU General Public License v3.0 * that is bundled with this package in the file license.txt. * It is also available through the world-wide-web at this URL: * http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later * If you did not receive a copy of the license and are unable to * obtain it through the world-wide-web, please send an email * to license@skyverge.com so we can send you a copy immediately. * * @since 3.0.0 * @author WooCommerce / SkyVerge / Delicious Brains * @copyright Copyright (c) 2015-2016 Delicious Brains Inc. * @copyright Copyright (c) 2013-2019, SkyVerge, Inc. * @license http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later * * Modified by WooCommerce on 01 December 2021. */ namespace WooCommerce\Square\Framework\Utilities; use WooCommerce\Square\Framework\Plugin_Compatibility; defined( 'ABSPATH' ) or exit; /** * Square WordPress Background Job Handler class * * Based on the wonderful WP_Background_Process class by deliciousbrains: * https://github.com/A5hleyRich/wp-background-processing * * Subclasses SV_WP_Async_Request. Instead of the concept of `batches` used in * the Delicious Brains' version, however, this takes a more object-oriented approach * of background `jobs`, allowing greater control over manipulating job data and * processing. * * A batch implicitly expected an array of items to process, whereas a job does * not expect any particular data structure (although it does default to * looping over job data) and allows subclasses to provide their own * processing logic. * * @since 3.0.0 */ abstract class Background_Job_Handler { /** @var string async request prefix */ protected $prefix = 'sv_wp'; /** @var string async request action */ protected $action = 'background_job'; /** @var string data key */ protected $data_key = 'data'; /** @var int start time of current process */ protected $start_time = 0; /** @var string cron hook identifier */ protected $cron_hook_identifier; /** @var string cron interval identifier */ protected $cron_interval_identifier; /** @var string debug message, used by the system status tool */ protected $debug_message; /** @var string job identifier */ protected $identifier; /** * Initiate new background job handler * * @since 3.0.0 */ public function __construct() { $this->identifier = $this->prefix . '_' . $this->action; $this->cron_hook_identifier = $this->identifier . '_cron'; $this->cron_interval_identifier = $this->identifier . '_cron_interval'; $this->add_hooks(); } /** * Adds the necessary action and filter hooks. * * @since 3.0.0 */ protected function add_hooks() { // cron healthcheck add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); // debugging & testing add_filter( 'gettext', array( $this, 'translate_success_message' ), 10, 3 ); } /** * Check whether job queue is empty or not * * @since 3.0.0 * @return bool True if queue is empty, false otherwise */ protected function is_queue_empty() { global $wpdb; $key = $this->identifier . '_job_%'; // only queued or processing jobs count $queued = '%"status":"queued"%'; $processing = '%"status":"processing"%'; $count = $wpdb->get_var( $wpdb->prepare( " SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s AND ( option_value LIKE %s OR option_value LIKE %s ) ", $key, $queued, $processing ) ); return ( $count > 0 ) ? false : true; } /** * Check whether background process is running or not * * Check whether the current process is already running * in a background process. * * @since 3.0.0 * @return bool True if processing is running, false otherwise */ protected function is_process_running() { // add a random artificial delay to prevent a race condition if 2 or more processes are trying to // process the job queue at the very same moment in time and neither of them have yet set the lock // before the others are calling this method usleep( wp_rand( 100000, 300000 ) ); return (bool) get_transient( "{$this->identifier}_process_lock" ); } /** * Lock process * * Lock the process so that multiple instances can't run simultaneously. * Override if applicable, but the duration should be greater than that * defined in the time_exceeded() method. * * @since 3.0.0 */ protected function lock_process() { // set start time of current process $this->start_time = time(); // set lock duration to 1 minute by default $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; /** * Filter the queue lock time * * @since 3.0.0 * @param int $lock_duration Lock duration in seconds */ $lock_duration = apply_filters( "{$this->identifier}_queue_lock_time", $lock_duration ); set_transient( "{$this->identifier}_process_lock", microtime(), $lock_duration ); } /** * Unlock process * * Unlock the process so that other instances can spawn. * * @since 3.0.0 * @return Background_Job_Handler */ protected function unlock_process() { delete_transient( "{$this->identifier}_process_lock" ); return $this; } /** * Check if memory limit is exceeded * * Ensures the background job handler process never exceeds 90% * of the maximum WordPress memory. * * @since 3.0.0 * * @return bool True if exceeded memory limit, false otherwise */ protected function memory_exceeded() { $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory $current_memory = memory_get_usage( true ); $return = false; if ( $current_memory >= $memory_limit ) { $return = true; } /** * Filter whether memory limit has been exceeded or not * * @since 3.0.0 * * @param bool $exceeded */ return apply_filters( "{$this->identifier}_memory_exceeded", $return ); } /** * Get memory limit * * @since 3.0.0 * * @return int memory limit in bytes */ protected function get_memory_limit() { if ( function_exists( 'ini_get' ) ) { $memory_limit = ini_get( 'memory_limit' ); } else { // sensible default $memory_limit = '128M'; } if ( ! $memory_limit || -1 === (int) $memory_limit ) { // unlimited, set to 32GB $memory_limit = '32G'; } return Plugin_Compatibility::convert_hr_to_bytes( $memory_limit ); } /** * Create a background job * * Delicious Brains' versions alternative would be using ->data()->save(). * Allows passing in any kind of job attributes, which will be available at item data processing time. * This allows sharing common options between items without the need to repeat * the same information for every single item in queue. * * Instead of returning self, returns the job instance, which gives greater * control over the job. * * @since 3.0.0 * * @param array|mixed $attrs Job attributes. * @return \stdClass|object|null */ public function create_job( $attrs ) { global $wpdb; if ( empty( $attrs ) ) { return null; } // generate a unique ID for the job $job_id = md5( microtime() . wp_rand() ); /** * Filter new background job attributes * * @since 3.0.0 * * @param array $attrs Job attributes * @param string $id Job ID */ $attrs = apply_filters( "{$this->identifier}_new_job_attrs", $attrs, $job_id ); // ensure a few must-have attributes $attrs = wp_parse_args( array( 'id' => $job_id, 'created_at' => current_time( 'mysql' ), 'created_by' => get_current_user_id(), 'status' => 'queued', ), $attrs ); $wpdb->insert( $wpdb->options, array( 'option_name' => "{$this->identifier}_job_{$job_id}", 'option_value' => wp_json_encode( $attrs ), 'autoload' => 'no' ) ); $job = new \stdClass(); foreach ( $attrs as $key => $value ) { $job->{$key} = $value; } /** * Runs when a job is created. * * @since 3.0.0 * * @param \stdClass|object $job the created job */ do_action( "{$this->identifier}_job_created", $job ); return $job; } /** * Get a job (by default the first in the queue) * * @since 3.0.0 * * @param string $id Optional. Job ID. Will return first job in queue if not * provided. Will not return completed or failed jobs from queue. * @return \stdClass|object|null The found job object or null */ public function get_job( $id = null ) { global $wpdb; if ( ! $id ) { $key = $this->identifier . '_job_%'; $queued = '%"status":"queued"%'; $processing = '%"status":"processing"%'; $results = $wpdb->get_var( $wpdb->prepare( " SELECT option_value FROM {$wpdb->options} WHERE option_name LIKE %s AND ( option_value LIKE %s OR option_value LIKE %s ) ORDER BY option_id ASC LIMIT 1 ", $key, $queued, $processing ) ); } else { $results = $wpdb->get_var( $wpdb->prepare( " SELECT option_value FROM {$wpdb->options} WHERE option_name = %s ", "{$this->identifier}_job_{$id}" ) ); } if ( ! empty( $results ) ) { $job = new \stdClass(); foreach ( json_decode( $results, true ) as $key => $value ) { $job->{$key} = $value; } } else { return null; } /** * Filters the job as returned from the database. * * @since 3.0.0 * * @param \stdClass|object $job */ return apply_filters( "{$this->identifier}_returned_job", $job ); } /** * Gets jobs. * * @since 3.0.0 * * @param array $args { * Optional. An array of arguments * * @type string|array $status Job status(es) to include * @type string $order ASC or DESC. Defaults to DESC * @type string $orderby Field to order by. Defaults to option_id * } * @return \stdClass[]|object[]|null Found jobs or null if none found */ public function get_jobs( $args = array() ) { global $wpdb; $args = wp_parse_args( $args, array( 'order' => 'DESC', 'orderby' => 'option_id', ) ); $replacements = array( $this->identifier . '_job_%' ); $status_query = ''; // prepare status query if ( ! empty( $args['status'] ) ) { $statuses = (array) $args['status']; $placeholders = array(); foreach ( $statuses as $status ) { $placeholders[] = '%s'; $replacements[] = '%"status":"' . sanitize_key( $status ) . '"%'; } $status_query = 'AND ( option_value LIKE ' . implode( ' OR option_value LIKE ', $placeholders ) . ' )'; } // prepare sorting vars $order = sanitize_key( $args['order'] ); $orderby = sanitize_key( $args['orderby'] ); // put it all together now // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Data is already sanitized to direct use. $results = $wpdb->get_col( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name LIKE %s {$status_query} ORDER BY {$orderby} {$order}", $replacements ) ); // phpcs:enable if ( empty( $results ) ) { return null; } $jobs = array(); foreach ( $results as $result ) { $job = new \stdClass(); foreach ( json_decode( $result, true ) as $key => $value ) { $job->{$key} = $value; } /* This filter is documented above. */ $job = apply_filters( "{$this->identifier}_returned_job", $job ); $jobs[] = $job; } return $jobs; } /** * Update job attrs * * @since 3.0.0 * * @param \stdClass|object|string $job Job instance or ID * @return \stdClass|object|false on failure */ public function update_job( $job ) { if ( is_string( $job ) ) { $job = $this->get_job( $job ); } if ( ! $job ) { return false; } $job->updated_at = current_time( 'mysql' ); $this->update_job_option( $job ); /** * Runs when a job is updated. * * @since 3.0.0 * * @param \stdClass|object $job the updated job */ do_action( "{$this->identifier}_job_updated", $job ); return $job; } /** * Handles job completion. * * @since 3.0.0 * * @param \stdClass|object|string $job Job instance or ID * @return \stdClass|object|false on failure */ public function complete_job( $job ) { if ( is_string( $job ) ) { $job = $this->get_job( $job ); } if ( ! $job ) { return false; } $job->status = 'completed'; $job->completed_at = current_time( 'mysql' ); $this->update_job_option( $job ); /** * Runs when a job is completed. * * @since 3.0.0 * * @param \stdClass|object $job the completed job */ do_action( "{$this->identifier}_job_complete", $job ); return $job; } /** * Handle job failure * * Default implementation does not call this method directly, but it's * provided as a convenience method for subclasses that may call this to * indicate that a particular job has failed for some reason. * * @since 3.0.0 * * @param \stdClass|object|string $job Job instance or ID * @param string $reason Optional. Reason for failure. * @return \stdClass|false on failure */ public function fail_job( $job, $reason = '' ) { if ( is_string( $job ) ) { $job = $this->get_job( $job ); } if ( ! $job ) { return false; } $job->status = 'failed'; $job->failed_at = current_time( 'mysql' ); if ( $reason ) { $job->failure_reason = $reason; } $this->update_job_option( $job ); /** * Runs when a job is failed. * * @since 3.0.0 * * @param \stdClass|object $job the failed job */ do_action( "{$this->identifier}_job_failed", $job ); return $job; } /** * Delete a job * * @since 3.0.0 * * @param \stdClass|object|string $job Job instance or ID * @return bool|void returns false on failure */ public function delete_job( $job ) { global $wpdb; if ( is_string( $job ) ) { $job = $this->get_job( $job ); } if ( ! $job ) { return false; } $wpdb->delete( $wpdb->options, array( 'option_name' => "{$this->identifier}_job_{$job->id}" ) ); /** * Runs after a job is deleted. * * @since 3.0.0 * * @param \stdClass|object $job the job that was deleted from database */ do_action( "{$this->identifier}_job_deleted", $job ); } /** * Handle job queue completion * * Override if applicable, but ensure that the below actions are * performed, or, call parent::complete(). * * @since 3.0.0 */ protected function complete() { // unschedule the cron healthcheck $this->clear_scheduled_event(); } /** * Schedule cron healthcheck * * @since 3.0.0 * @param array $schedules * @return array */ public function schedule_cron_healthcheck( $schedules ) { $interval = property_exists( $this, 'cron_interval' ) ? $this->cron_interval : 5; /** * Filter cron health check interval * * @since 3.0.0 * @param int $interval Interval in minutes */ $interval = apply_filters( "{$this->identifier}_cron_interval", $interval ); // adds every 5 minutes to the existing schedules. $schedules[ $this->identifier . '_cron_interval' ] = array( 'interval' => MINUTE_IN_SECONDS * $interval, /* translators: %d: interval in minutes */ 'display' => sprintf( esc_html__( 'Every %d Minutes', 'woocommerce-square' ), $interval ), ); return $schedules; } /** * Schedule cron health check event * * @since 3.0.0 */ protected function schedule_event() { /** * Filters the interval of sync healthcheck action. * * @since 3.8.2 * * @param int $interval sync heathcheck interval in seconds (defaults to 5 minutes) */ $interval = apply_filters( 'wc_square_sync_healthcheck_interval', 300 ); $group = 'square'; if ( false === as_next_scheduled_action( $this->cron_hook_identifier, array(), $group ) ) { // Schedule the health check to fire after 5 mins from now, and run every 5 mins until queue get empty. as_schedule_recurring_action( time() + $interval, $interval, $this->cron_hook_identifier, array(), $group ); } } /** * Clear scheduled health check event * * @since 3.0.0 */ protected function clear_scheduled_event() { as_unschedule_all_actions( $this->cron_hook_identifier ); } /** * Process an item from job data * * Implement this method to perform any actions required on each * item in job data. * * @since 3.0.0 * * @param mixed $item Job data item to iterate over * @param \stdClass|object $job Job instance * @return mixed */ abstract protected function process_item( $item, $job ); /** * Handles PHP shutdown, say after a fatal error. * * @since 3.0.0 * * @param \stdClass|object $job the job being processed */ public function handle_shutdown( $job ) { $error = error_get_last(); // if shutting down because of a fatal error, fail the job if ( $error && E_ERROR === $error['type'] ) { $this->fail_job( $job, $error['message'] ); $this->unlock_process(); } } /** * Update a job option in options database. * * @since 3.0.0 * * @param \stdClass|object $job the job instance to update in database * @return int|bool number of rows updated or false on failure, see wpdb::update() */ private function update_job_option( $job ) { global $wpdb; return $wpdb->update( $wpdb->options, array( 'option_value' => wp_json_encode( $job ) ), array( 'option_name' => "{$this->identifier}_job_{$job->id}" ) ); } /** Debug & Testing Methods ***********************************************/ /** * Translate the tool success message. * * This can be removed in favor of returning the message string in `run_debug_tool()` * when WC 3.1 is required, though that means the message will always be "success" styled. * * @since 3.0.0 * * @param string $translated the text to output * @param string $original the original text * @param string $domain the textdomain * @return string the updated text */ public function translate_success_message( $translated, $original, $domain ) { if ( 'woocommerce' === $domain && ( 'Tool ran.' === $original || 'There was an error calling %s' === $original ) ) { $translated = $this->debug_message; } return $translated; } /** Helper Methods ********************************************************/ /** * Gets the job handler identifier. * * @since 3.0.0 * * @return string */ public function get_identifier() { return $this->identifier; } }
💾 保存
取消
保存为 UTF-8,换行统一为 LF