[1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], * 'feed_name' => ${option_name}, * 'hook_name' => "woo_feed_update_${option_name}" * ) * * And another cache will be "woo_feed_sub_cron_list" which will be an array of all the sub-batch cron job names. * Example array( * "wf_store_auto_feed_body_info_${option_name}_0", * "wf_store_auto_feed_body_info_${option_name}_1", * "wf_store_auto_feed_body_info_${option_name}_2", * "wf_store_auto_feed_body_info_${option_name}_3", * "wf_store_auto_feed_body_info_${option_name}_4", * ) * * Sub bath cron job cache will be like this "wf_store_auto_feed_body_info_${option_name}_0" => array( * 'feed_name' => "${option_name}", * 'option' => "", * 'total_offset' => 1, * 'parent_hook_name' => "woo_feed_update_${option_name}", * 'product_ids' => [1, 3, 4, 5, ... ], * 'offset' => 0, * 'hook_name' => "wf_store_auto_feed_body_info_${option_name}_0", * ); * * Now when the parent cron job will be executed it will get the product ids from the cache "woo_feed_update_${option_name}". * Then it will create sub-batches for the product ids. And will create sub-batch cron jobs. Batch will be created depending on the product ids. * If product ids are 100000 then 20 sub-batches will be created. Because the product batch safe limit is 5000. The execution time sub batches are * 1rst sub-batch will be executed immediately. And the 2nd the sub-batches will be executed after 1 minute. And the 3rd sub-batches will be executed * 2 minutes later. And so on. And the last sub-batch will be executed after 19 minutes. When finally the last sub-batch will be executed then all * files will be merged with header and footer into 1 file named "${option_name}.${file_extentsion}". And all the sub-batch files will be deleted. * * Now all sub-batch cron jobs will be executed after 1 year. And the parent cron job will be executed after 1 hour. */ /** * AI Generated Feed Cron Job Documentation for CTX Feed Plugin Feed Generation. * * WordPress Cron Job Documentation for CTX Feed Plugin Feed Generation. * * This cron job automates the generation of feed files (XML, CSV, TXT, etc.) for Google Shopping, Facebook Catalog, etc., * from a vast pool of 100,000 Woo Commerce products. The process is designed to overcome memory and execution time * constraints by splitting it into a parent cron job and sub-batch cron jobs. * * @since 7.0.0 * @see woo_feed_update_${option_name} * @see wf_store_auto_feed_body_info_${option_name}_0, wf_store_auto_feed_body_info_${option_name}_1, ... * * ## Parent Cron Job: * - **Name:** `woo_feed_update_${option_name}` * - **Interval:** Default if products < $cron_job_per_batch, else 1 hour. * - **Cache:** Stored in `woo_feed_cron_list` for 1 year. * - **Cache Content (Example):** * ``` * array( * 'product_ids' => [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], * 'feed_name' => ${option_name}, * 'hook_name' => "woo_feed_update_${option_name}" * ) * ``` * * ## Sub-Batch Cron Jobs: * - **Name Format:** `wf_store_auto_feed_body_info_${option_name}_0`, ... * - **Interval:** 1 year (manually triggered before the parent cron job). * - **Cache:** Stored in `woo_feed_sub_cron_list`. * - **Cache Content (Example):** * ``` * array( * "wf_store_auto_feed_body_info_${option_name}_0", * "wf_store_auto_feed_body_info_${option_name}_1", * ... * ) * ``` * * ## Sub-Batch Cron Job Cache: * - **Cache Format:** `wf_store_auto_feed_body_info_${option_name}_0` * - **Content (Example):** * ``` * array( * 'feed_name' => "${option_name}", * 'option' => "", * 'total_offset' => 1, * 'parent_hook_name' => "woo_feed_update_${option_name}", * 'product_ids' => [1, 3, 4, 5, ... ], * 'offset' => 0, * 'hook_name' => "wf_store_auto_feed_body_info_${option_name}_0", * ) * ``` * * ## Execution Flow: * - Parent cron job fetches product IDs from either `woo_feed_update_${option_name}` cache or QueryFactor class based on $config object. * - Sub-batches are created with staggered execution times. * - Sub-batch cron jobs are scheduled for immediate and 1-minute interval execution. * - The last sub-batch executes after 19 minutes. * - Files are merged into `${option_name}.${file_extension}`, and sub-batch files are deleted. * - Sub-batch cron jobs run after 1 year, and the parent cron job runs after 1 hour. */ /** * Class CronHelper * @package CTXFeed\V5\Helper * * @since 7.3.0 */ class CronHelper { // phpcs:ignore /** * Cron Settings. * * @var array */ private static $settings = array(); /** * Product Batch Safe Limit. * * @var int */ private static $cron_job_per_batch = 5000; /** * Cache Expiration. * * @var int */ private static $cache_expiration = YEAR_IN_SECONDS; /** * Cache Prefix. * * @var string */ private static $cache_prefix = '__woo_feed_auto_update_cache_'; /** * Constructor */ public function __construct() { $cron_per_batch_setting = Settings::get( 'cron_job__per_batch__safe_limit' ); if($cron_per_batch_setting) { self::$cron_job_per_batch = $cron_per_batch_setting; } if (!isset(self::$settings['sub_feed_body_prefix'])) { self::set_cron_settings(); } // Delete current feed cron jobs. add_action( 'woo_feed_before_product_loop', array( $this, 'clear_cron_schedule_before_product_query' ), 10, 3 ); add_action( 'woo_feed_after_product_loop', array( $this, 'schedule_cron_job_after_product_query' ), 10, 3 ); // Add cron job for all feeds. $feed_cron_list = self::get_cache( 'woo_feed_cron_list' ); if ( ! empty( $feed_cron_list ) ) { foreach ( $feed_cron_list as $feed_cron_hook_name ) { add_action( $feed_cron_hook_name, array( $this, 'woo_feed_cron_update_single_feed' ), 10, 1 ); } } // Add cron job for all feeds sub batches. $feed_sub_cron_list = self::get_cache( 'woo_feed_sub_cron_list' ); if ( ! empty( $feed_sub_cron_list ) ) { foreach ( $feed_sub_cron_list as $feed_sub_cron_hook_name ) { add_action( $feed_sub_cron_hook_name, array( $this, 'woo_feed_cron_update_batched_feed' ), 10, 1 ); } } // Handle old feed. add_action( 'woo_feed_update_single_feed', array( $this, 'handle_old_single_feed' ), 10, 1 ); // Delete old woo_feed_update cron job. if ( ! self::get_cache( 'is_deleted__woo_feed_update__cron_job' ) || self::is_cron_exits( 'woo_feed_update' ) ) { self::delete_cron_job( 'woo_feed_update' ); return; } self::set_cache( 'is_deleted__woo_feed_update__cron_job', true ); } /** * Get Cron Hook Name. * * @param string $feed_name Feed Name. * @param bool $is_single_hook Is Single Hook. * * @return string */ public static function get_cron_hook_name( $feed_name, $is_single_hook = false ) { $feed_name = str_replace( 'wf_feed_', '', $feed_name ); $feed_name = str_replace( 'wf_config', '', $feed_name ); if(!isset(self::$settings['single_feed_hook_prefix'])) { self::set_cron_settings(); } if ( $is_single_hook ) { if ( strpos( $feed_name, self::$settings['single_feed_hook_prefix'] ) === false ) { $hook_name = self::$settings['single_feed_hook_prefix'] . $feed_name; } else { $hook_name = $feed_name; } } elseif ( strpos( $feed_name, self::$settings['sub_feed_body_prefix'] ) === false ) { $hook_name = self::$settings['sub_feed_body_prefix'] . $feed_name; } else { $hook_name = $feed_name; } return $hook_name; } /** * Delete Cron Job. * * @param string $hook_name Cron Hook Name. * * @return void */ public static function delete_cron_job( $hook_name, $arg_value = false ) { // !IMPORTANT:: delete also sub hooks. $crons = _get_cron_array(); $updated = array_filter( $crons, function ( $v ) use ( $hook_name, $arg_value ) { // If arg value is set then check with arg value. if ( $arg_value && array_key_exists( $hook_name, $v ) ) { $cron_job = $v[ $hook_name ]; $args = array_values( $cron_job ); foreach ( $args as $arg ) { if ( isset( $arg['args'] ) && in_array( $arg_value, $arg['args'] ) ) { return false; } } return true; } else { return ! array_key_exists( $hook_name, $v ); } } ); _set_cron_array( $updated ); } /** * Delete Cron Job. * * @param string $hook_name Cron Hook Name. * * @return bool */ public static function is_cron_exits( $hook_name ) { // !IMPORTANT:: delete also sub hooks. $crons = _get_cron_array(); $is_cron_exists = array_filter( $crons, function ( $v ) use ( $hook_name ) { return array_key_exists( $hook_name, $v ); } ); return ! empty( $is_cron_exists ); } /** * Add Cron Job. * * @param string $hook_name Cron Hook Name. * @param bool $is_single_hook Is Single Hook. * * @return void */ public static function add_cron_job( $hook_name, $is_single_hook ) { $hook_name = self::get_cron_hook_name( $hook_name, $is_single_hook ); self::delete_cron_job( $hook_name ); if ( wp_next_scheduled( $hook_name, array( $hook_name ) ) ) { return; } $interval = self::get_feed_cron_interval(); wp_schedule_event( time() + $interval, 'woo_feed_corn', $hook_name, array( $hook_name ) ); } /** * Schedule Cron Job For Sub Batches. * * @param string $feed_name Feed Name. * @param array $cron_batched_ids Cron Batched Ids. * @param array $batch_data Batch Data. * @param array $option Feed Option. * @param bool $execute_now Execute Now or Not. * * @return void */ private function schedule_cron_job_for_sub_batches( // phpcs:ignore $feed_name, $cron_batched_ids, $batch_data, $option, $execute_now = false ) { // phpcs:ignore $feed_name = str_replace( 'wf_config', 'wf_feed_', $feed_name ); $time = 0; $current_batch_data = array( 'feed_name' => $feed_name, 'option' => $option, 'total_offset' => count( $cron_batched_ids ) - 1, 'parent_hook_name' => $batch_data['hook_name'], ); if (!isset(self::$settings['sub_feed_body_prefix'])) { self::set_cron_settings(); } foreach ( $cron_batched_ids as $index => $batch_ids ) { $current_batch_name = $feed_name . '_' . $index; $current_batch_feed_cron_hook_name = self::$settings['sub_feed_body_prefix'] . $current_batch_name; $current_batch_data['product_ids'] = $batch_ids; $current_batch_data['offset'] = $index; $current_batch_data['hook_name'] = $current_batch_feed_cron_hook_name; // Delete sub batch cron job. And reschedule it. self::delete_cron_job( $current_batch_feed_cron_hook_name ); if ( wp_next_scheduled( $current_batch_feed_cron_hook_name, array( $current_batch_feed_cron_hook_name ) ) ) { continue; } // Delete sub batch data if exists. And set new data. if ( self::get_cache( $current_batch_feed_cron_hook_name ) ) { self::delete_cache( $current_batch_feed_cron_hook_name ); } self::set_cache( $current_batch_feed_cron_hook_name, wp_json_encode( $current_batch_data ) ); if ( $execute_now ) { if ( $index > 0 ) { $time += self::$settings['sub_batch_update_interval']; } wp_schedule_event( time() + $time, 'woo_feed_corn', $current_batch_feed_cron_hook_name, array( $current_batch_feed_cron_hook_name ) ); } else { wp_schedule_event( time() + YEAR_IN_SECONDS, 'woo_feed_corn', $current_batch_feed_cron_hook_name, array( $current_batch_feed_cron_hook_name ) ); } } } /** * Clear Cron Schedule Before Update Config. * * @param array $product_ids Product Ids. * @param array $feed_rules Feed Rules. * @param \CTXFeed\V5\Utility\Config $config Config. * * @return void */ public function clear_cron_schedule_before_product_query( $product_ids, $feed_rules, $config ) { // phpcs:ignore $feed_name = $config->get_feed_option_name(); if ( ! $feed_name ) { return; } if (!isset(self::$settings['single_feed_hook_prefix'])) { self::set_cron_settings(); } self::delete_cron_job( self::$settings['single_feed_hook_prefix'] . $feed_name ); } /** * Schedule Cron Job After Product Query. * * @param array $product_ids Product Ids. * @param array $feed_rules Feed Rules. * @param \CTXFeed\V5\Utility\Config $config Config. * * @return void */ public function schedule_cron_job_after_product_query( $product_ids, $feed_rules, $config ) { // phpcs:ignore $cron_batched_ids = $this->get_cron_batches( $product_ids ); $interval = self::get_feed_cron_interval( $product_ids ); $feed_name = $config->get_feed_option_name(); if ( ! $feed_name ) { return; } if (!isset(self::$settings['single_feed_hook_prefix'])) { self::set_cron_settings(); } $feed_cron_hook_name = self::$settings['single_feed_hook_prefix'] . $feed_name; $batch_data = array( 'product_ids' => $product_ids, 'feed_name' => $feed_name, 'hook_name' => $feed_cron_hook_name, ); if ( ! wp_next_scheduled( $feed_cron_hook_name, array( $feed_cron_hook_name ) ) ) { if ( self::get_cache( $feed_cron_hook_name ) ) { self::delete_cache( $feed_cron_hook_name ); } self::set_cache( $feed_cron_hook_name, wp_json_encode( $batch_data ) ); if ( self::get_cache( 'woo_feed_cron_list' ) ) { $feed_cron_list = self::get_cache( 'woo_feed_cron_list' ); if ( is_array( $feed_cron_list ) && ! in_array( $feed_cron_hook_name, $feed_cron_list ) ) { // phpcs:ignore $feed_cron_list[] = $feed_cron_hook_name; self::set_cache( 'woo_feed_cron_list', $feed_cron_list ); } } else { self::set_cache( 'woo_feed_cron_list', array( $feed_cron_hook_name ) ); } wp_schedule_event( time() + $interval, 'woo_feed_corn', $feed_cron_hook_name, array( $feed_cron_hook_name ) ); } if ( ! count( $cron_batched_ids ) ) { return; } foreach ( $cron_batched_ids as $index => $batch_ids ) { // phpcs:ignore $current_batch_name = $feed_name . '_' . $index; $current_batch_feed_cron_hook_name = self::$settings['sub_feed_body_prefix'] . $current_batch_name; self::delete_cron_job( $current_batch_feed_cron_hook_name ); if ( wp_next_scheduled( $current_batch_feed_cron_hook_name, array( $current_batch_feed_cron_hook_name ) ) ) { continue; } if ( self::get_cache( 'woo_feed_sub_cron_list' ) ) { $feed_sub_cron_list = self::get_cache( 'woo_feed_sub_cron_list' ); if ( is_array( $feed_sub_cron_list ) && ! in_array( $current_batch_feed_cron_hook_name, $feed_sub_cron_list ) // phpcs:ignore ) { // phpcs:ignore $feed_sub_cron_list[] = $current_batch_feed_cron_hook_name; self::set_cache( 'woo_feed_sub_cron_list', $feed_sub_cron_list ); } } else { self::set_cache( 'woo_feed_sub_cron_list', array( $current_batch_feed_cron_hook_name ) ); } wp_schedule_event( time() + YEAR_IN_SECONDS, 'woo_feed_corn', $current_batch_feed_cron_hook_name, array( $current_batch_feed_cron_hook_name ) ); } } /** * Scheduled Action Hook For Old Single Cron Jobs. * * @param string $option_name Old single cron job name which will be 'woo_feed_update_single_feed'. * * @return void */ public function handle_old_single_feed( $option_name ) { $results = Feed::get_single_feed( $option_name ); // phpcs:ignore // If results variable is empty then check with "AttributeValueByType::FEED_RULES_OPTION_PREFIX" prefix. if ( empty( $results ) ) { $option_name_with_prefix = AttributeValueByType::FEED_RULES_OPTION_PREFIX . $option_name; $results = Feed::get_single_feed( $option_name_with_prefix ); // phpcs:ignore // If results variable is empty then check with "wf_config" prefix. if ( empty( $results ) ) { $option_name_with_prefix = 'wf_config' . $option_name; $results = Feed::get_single_feed( $option_name_with_prefix ); // phpcs:ignore // If results variable is empty then delete the "woo_feed_update_single_feed". if ( empty( $results ) ) { self::delete_cron_job( 'woo_feed_update_single_feed', $option_name ); return; } } } $feed_info = $results[0]; if ( ! isset( $feed_info['option_value']['feedrules'] ) ) { self::delete_cron_job( 'woo_feed_update_single_feed', $option_name ); return; } $config = new Config( $feed_info ); // Hook Before Query Products do_action( 'before_woo_feed_get_product_information', $config ); // Get Product Ids $ids = QueryFactory::get_ids( $config, array() ); // Hook After Query Products do_action( 'after_woo_feed_get_product_information', $config ); if ( ! empty( $ids ) ) { $this->schedule_cron_job_after_product_query( $ids, $feed_info['option_value']['feedrules'], $config ); } self::delete_cron_job( 'woo_feed_update_single_feed', $option_name ); } /** * Execute Single Feed Cron Job And Schedule Sub Batch Cron Jobs. * * @param string $cache_key Single cron job key. * * @return void */ public function woo_feed_cron_update_single_feed( $cache_key ) { $batch_data = self::get_cache( $cache_key ); $batch_data = json_decode( $batch_data, true ); if ( empty( $batch_data ) || ! isset( $batch_data['feed_name'] ) || ! isset( $batch_data['hook_name'] ) || ! isset( $batch_data['product_ids'] ) ) { return; } $feed_name = $batch_data['feed_name']; $feed_option_name = AttributeValueByType::FEED_RULES_OPTION_PREFIX . $feed_name; $results = Feed::get_single_feed( $feed_option_name ); // phpcs:ignore if ( empty( $results ) ) { $hook_name = $batch_data['hook_name']; self::delete_cron_job( $hook_name ); return; } $option = $results[0]; $feed_info = $option['option_value']; if ( ! isset( $feed_info['feedrules'] ) || isset( $feed_info['status'] ) && '0' === $feed_info['status'] ) { return; } $config = new Config( $option ); // Hook Before Query Products do_action( 'before_woo_feed_get_product_information', $config ); // Get Product Ids $ids = QueryFactory::get_ids( $config, array() ); // Get Cron Batches $batch_data['product_ids'] = $ids; self::delete_cache( $batch_data['hook_name'] ); self::set_cache( $batch_data['hook_name'], wp_json_encode( $batch_data ) ); $cron_batched_ids = $this->get_cron_batches( $ids ); // Hook After Query Products do_action( 'after_woo_feed_get_product_information', $config ); if ( ! count( $cron_batched_ids ) ) { return; } $this->schedule_cron_job_for_sub_batches( $feed_name, $cron_batched_ids, $batch_data, $option, true ); } /** * Execute Sub Batch Cron Job And Merge All Sub Batched Feed Files Into A Single Feed File. * * @param string $cache_key Sub batch cache key. * * @return void */ public function woo_feed_cron_update_batched_feed( $cache_key ) { // phpcs:ignore $batch_data = self::get_cache( $cache_key ); $batch_data = json_decode( $batch_data, true ); // Check if the batch data is valid. if ( empty( $batch_data ) || ! isset( $batch_data['product_ids'] ) || ! isset( $batch_data['offset'] ) || ! isset( $batch_data['option'] ) || ! isset( $batch_data['total_offset'] ) || ! isset( $batch_data['parent_hook_name'] ) ) { return; } $product_ids = $batch_data['product_ids']; $offset = $batch_data['offset']; $option = $batch_data['option']; $feed_info = $option['option_value']; $should_update_last_update_time = false; if ( $offset === $batch_data['total_offset'] ) { $should_update_last_update_time = true; } try { $option = FeedHelper::validate_feed( $option ); // Create a new prefix for the current feed body. add_filter( 'woo_feed_temp_feed_body_prefix', function ( $prefix ) use ( $offset ) { if ( $offset === 0 ) { $prefix_arr = $prefix . $offset . '_'; } else { $prefix_arr = explode( '_', $prefix ); foreach ( $prefix_arr as $key => $value ) { if ( ! is_numeric( $value ) ) { continue; } unset( $prefix_arr[ $key ] ); } $prefix_arr = implode( '_', $prefix_arr ) . $offset . '_'; } return $prefix_arr; }, 999, 1 ); // Generate feed for the current sub batch. FeedHelper::generate_cron_batched_feed( $option, $offset, $should_update_last_update_time, $product_ids ); } catch ( \CTXFeed\V5\Helper\Exception $e ) { $message = 'Error Updating Feed Via CRON Job' . PHP_EOL . 'Caught Exception :: ' . $e->getMessage(); Logs::write_log( $feed_info['feedrules']['filename'], $message, 'critical', $e, true ); Logs::write_fatal_log( $message, $e ); } /** * IF current sub batch is last batch for the feed, then update the last update time. * And merge all the sub batched feed files into a single feed file. * Then delete all the sub batched feed files. */ if ( ! $should_update_last_update_time ) { return; } FeedHelper::save_cron_batched_feed_files( $option, $should_update_last_update_time, true ); $parent_hook_name = $batch_data['parent_hook_name']; $parent_batch_data = self::get_cache( $parent_hook_name ); $parent_batch_data = json_decode( $parent_batch_data, true ); $cron_batches = $this->get_cron_batches( $parent_batch_data['product_ids'] ); // Reschedule parent cron job. self::delete_cron_job( $parent_hook_name ); $interval = self::get_feed_cron_interval( $parent_batch_data['product_ids'] ); wp_schedule_event( time() + $interval, 'woo_feed_corn', $parent_hook_name, array( $parent_hook_name ) ); $this->schedule_cron_job_for_sub_batches( $batch_data['feed_name'], $cron_batches, $parent_batch_data, $option, false ); } /** * Get Cron Batches * * @param array $product_ids of product ids. * * @return array */ public function get_cron_batches( $product_ids ) { return array_chunk( $product_ids, self::$settings['product_batch_safe_limit'] ); } /** * Get Feed Cron Interval * * !IMPORTANT Feed update interval should be single feed based. Currently it is global. * * @param array $product_ids of product ids. * * @return int */ private static function get_feed_cron_interval( $product_ids = array() ) { /** * !IMPORTANT: Feed update interval should be single feed based. Currently it is global. * If a single feed has 10000 products and another feed has 100 products, then the feed with 10000 products will take more time to update. * So, the feed with 100 products will be updated more frequently than the feed with 10000 products. */ $interval = absint( get_option( 'wf_schedule' ) ); if ( ! $interval ) { $interval = 3600; } if (!isset(self::$settings['product_batch_safe_limit'])) { self::set_cron_settings(); } $cron_job_per_batch = self::$cron_job_per_batch; if ( isset( self::$settings['product_batch_safe_limit'] ) ) { $cron_job_per_batch = self::$settings['product_batch_safe_limit']; } if ( count( $product_ids ) > $cron_job_per_batch && $interval < 3600 ) { $interval = 3600; } return apply_filters( 'woo_feed_cron_interval', $interval ); } /** * Set Cache Data For Cron Job. * * @param string $key Cache Key. * @param mixed $value Cache Value. * * @return void */ private static function set_cache( $key, $value ) { update_option( self::$cache_prefix . $key, $value ); } /** * Get Cache Data For Cron Job. * * @param string $key Cache Key. * * @return mixed */ private static function get_cache( $key ) { $cache = Cache::get( $key, self::$cache_prefix ); if ( $cache ) { self::set_cache( $key, $cache ); Cache::delete( $key, self::$cache_prefix ); } return get_option( self::$cache_prefix . $key ); } /** * Delete Cache Data For Cron Job. * * @param string $key Cache Key. */ private static function delete_cache( $key ) { Cache::delete( $key, self::$cache_prefix ); delete_option( self::$cache_prefix . $key ); } /** * Set Cron Settings. * * @return void */ public static function set_cron_settings() { self::$settings = apply_filters( 'woo_feed_cron_settings', array( 'product_batch_safe_limit' => self::$cron_job_per_batch, 'sub_batch_update_interval' => 10, 'sub_feed_body_prefix' => FeedHelper::get_feed_body_temp_prefix(true), 'single_feed_hook_prefix' => 'woo_feed_update_', ) ); } /** * Is Cron Enabled. * * @return bool */ private static function is_cron_enabled() { return apply_filters('ctx_feed_cron_enabled', Helper::should_init_new_cron_system()); } }