if (!$action_id) { throw new RuntimeException(esc_html_x('Failed to schedule the task.', 'This error is thrown when a task fails to be scheduled.', 'stellarwp-shepherd')); } $task->set_action_id($action_id); $this->scheduled_tasks[] = $task->save(); $log_data = ['action_id' => $action_id, 'current_try' => $task->get_current_try()]; if ($previous_action_id) { /** * Fires when a task should be retried. * * @since 0.0.1 * * @param Task $task The task that should be retried. */ do_action('shepherd_' . Config::get_hook_prefix() . '_task_rescheduled', $task); $this->log_rescheduled($task->get_id(), array_merge($log_data, ['previous_action_id' => $previous_action_id])); } else { /** * Fires when a task should be retried. * * @since 0.0.1 * * @param Task $task The task that should be retried. */ do_action('shepherd_' . Config::get_hook_prefix() . '_task_created', $task); $this->log_created($task->get_id(), $log_data); } DB::commit(); } catch (RuntimeException $e) { DB::rollback(); /** * Fires when a task fails to be scheduled or inserted into the database. * * @since 0.0.1 * * @param Task $task The task that failed to be scheduled or inserted into the database. * @param RuntimeException $e The exception that was thrown. */ do_action('shepherd_' . Config::get_hook_prefix() . '_task_scheduling_failed', $task, $e); } catch (ShepherdTaskAlreadyExistsException $e) { DB::rollback(); /** * Fires when a task is already scheduled. * * @since 0.0.1 * * @param Task $task The task that is already scheduled. */ do_action('shepherd_' . Config::get_hook_prefix() . '_task_already_scheduled', $task); } } /** * Run a set of tasks. * * @since 0.1.0 * * @param Task[] $tasks The tasks to run. * @param array $callables The callables to run. * * @phpstan-param array{} | array{ * before: callable( Task $task ): void, * after: callable( Task $task ): void, * always: callable( list $tasks ): void, * } $callables * * @return void */ public function run(array $tasks, array $callables = []): void { $prefix = Config::get_hook_prefix(); if (!did_action("shepherd_{$prefix}_tables_registered")) { foreach ($tasks as $task) { $task->process(); /** * Fires an action when a task is run synchronously. * * @since 0.1.0 * * @param Task $task The task that was dispatched synchronously. */ do_action("shepherd_{$prefix}_task_run_sync", $task); } return; } if (did_action('action_scheduler_init') || doing_action('action_scheduler_init')) { $this->run_callback($tasks, $callables); return; } add_action('action_scheduler_init', function () use ($tasks, $callables): void { $this->run_callback($tasks, $callables); }, 10); } /** * Runs a set of tasks. * * @since 0.1.0 * * @param Task[] $tasks The tasks to run. * @param array $callables The callables to run. * * @phpstan-param array{} | array{ * before: callable( Task $task ): void, * after: callable( Task $task ): void, * always: callable( list $tasks ): void, * } $callables * * @return void */ private function run_callback(array $tasks, array $callables = []): void { $callables = wp_parse_args($callables, ['before' => static function (Task $task): void { }, 'after' => static function (Task $task): void { }, 'always' => static function (array $tasks): void { }]); $context = defined('WP_CLI') && WP_CLI ? ' CLI' : ''; $context = !$context && defined('REST_REQUEST') && REST_REQUEST ? ' REST' : $context; $prefix = Config::get_hook_prefix(); $runner = ActionScheduler_QueueRunner::instance(); try { /** * Filters the number of tasks to clean up after. * * @since 0.1.0 * * @param int $clean_up_memory_every The number of tasks to clean up the memory after. * * @return int The number of tasks to clean up the memory after. */ $clean_up_memory_every = (int) apply_filters("shepherd_{$prefix}_clean_up_memory_every", 10); foreach (array_values($tasks) as $offset => $task) { if (!in_array($task->get_id(), $this->scheduled_tasks, true)) { $this->dispatch_callback($task, 0); } if (is_callable($callables['before'])) { $callables['before']($task); } /** * Fires when a task is about to be run. * * @since 0.1.0 * * @param Task $task The task that is about to be run. */ do_action("shepherd_{$prefix}_task_before_run", $task); $runner->process_action($task->get_action_id(), "Shepherd{$context}"); if (is_callable($callables['after'])) { $callables['after']($task); } /** * Fires when a task is finished running. * * @since 0.1.0 * * @param Task $task The task that is finished running. */ do_action("shepherd_{$prefix}_task_after_run", $task); if (0 === ($offset + 1) % $clean_up_memory_every) { $this->free_memory(); } } if (is_callable($callables['always'])) { $callables['always']($tasks); } /** * Fires when a set of tasks is finished running. * * @since 0.1.0 * * @param Task[] $tasks The tasks that were run. */ do_action("shepherd_{$prefix}_tasks_finished", $tasks); } catch (Throwable $e) { /** * Fires when a set of tasks fails to be run. * * @since 0.1.0 * * @param Task[] $tasks The tasks that failed to be run. * @param Throwable $e The exception that was thrown. */ do_action("shepherd_{$prefix}_tasks_run_failed", $tasks, $e); } } /** * Gets the last scheduled task ID. * * @since 0.0.1 * * @return ?int The last scheduled task ID. */ public function get_last_scheduled_task_id(): ?int { return empty($this->scheduled_tasks) ? null : end($this->scheduled_tasks); } /** * Gets the process task hook. * * @since 0.0.1 * * @return string The process task hook. */ public function get_hook(): string { return $this->process_task_hook; } /** * Busts the runtime cached tasks. * * @since 0.0.1 */ public function bust_runtime_cached_tasks(): void { $this->scheduled_tasks = []; } /** * Processes a task. * * @since 0.0.1 * @since 0.0.8 Made strings translatable. * * @param string $args_hash The arguments hash. * * @throws RuntimeException If no action ID is found, no Shepherd task is found with the action ID, or the task arguments hash does not match the expected hash. * @throws ShepherdTaskException If the task fails to be processed. * @throws ShepherdTaskFailWithoutRetryException If the task fails to be processed without retry. * @throws Throwable If the task fails to be processed. */ public function process_task(string $args_hash): void { $task = null; if (!$this->current_action_id) { $task = Tasks_Table::get_by_args_hash($args_hash); if (!$task) { // translators: %s is the arguments hash. throw new RuntimeException(sprintf(esc_html_x('No Shepherd task found with args hash %s.', 'This error is thrown when a task is not found with the arguments hash.', 'stellarwp-shepherd'), $args_hash)); } $task = array_shift($task); } $task ??= Tasks_Table::get_by_action_id($this->current_action_id); if (!$task) { // translators: %d is the action ID. throw new RuntimeException(sprintf(esc_html_x('No Shepherd task found with action ID %d.', 'This error is thrown when a task is not found with the action ID.', 'stellarwp-shepherd'), $this->current_action_id)); } $log_data = ['action_id' => $this->current_action_id, 'current_try' => $task->get_current_try()]; /** * Fires when a task is being processed. * * @since 0.0.1 * * @param Task $task The task that is being processed. * @param int $action_id The action ID that is being processed. */ do_action('shepherd_' . Config::get_hook_prefix() . '_task_started', $task, $this->current_action_id); try { try { if ($task->get_current_try() > 0) { $this->log_retrying($task->get_id(), $log_data); } else { $this->log_starting($task->get_id(), $log_data); } $task->process(); $this->log_finished($task->get_id(), $log_data); } catch (ShepherdTaskException $e) { throw $e; } } catch (ShepherdTaskFailWithoutRetryException $e) { /** * Fires when a task fails to be processed without retry. * * @since 0.0.1 * * @param Task $task The task that failed to be processed without retry. * @param ShepherdTaskFailWithoutRetryException $e The exception that was thrown. */ do_action('shepherd_' . Config::get_hook_prefix() . '_task_failed_without_retry', $task, $e); /** * Fires when a task fails to be processed without retry. * * @since 0.0.1 * * @param Task $task The task that failed to be processed without retry. * @param ShepherdTaskFailWithoutRetryException $e The exception that was thrown. */ do_action('shepherd_' . Config::get_hook_prefix() . '_task_failed_without_retry', $task, $e); $this->log_failed($task->get_id(), array_merge($log_data, ['exception' => $e->getMessage()])); throw $e; } catch (Throwable $e) { /** * Fires when a task fails to be processed. * * @since 0.0.1 * * @param Task $task The task that failed to be processed. * @param Throwable $e The exception that was thrown. */ do_action('shepherd_' . Config::get_hook_prefix() . '_task_failed', $task, $e); if ($this->should_retry($task)) { throw new ShepherdTaskException(esc_html_x('The task failed, but will be retried.', 'This error is thrown when a task fails to be processed, but will be retried.', 'stellarwp-shepherd')); } $this->log_failed($task->get_id(), array_merge($log_data, ['exception' => $e->getMessage()])); throw $e; } /** * Fires when a task is finished processing. * * @since 0.0.1 * * @param Task $task The task that is finished processing. * @param int $action_id The action ID that is finished processing. */ do_action('shepherd_' . Config::get_hook_prefix() . '_task_finished', $task, $this->current_action_id); } /** * Determines if the task should be retried. * * @since 0.0.1 * * @param Task $task The task. * @return bool Whether the task should be retried. */ protected function should_retry(Task $task): bool { if (0 === $task->get_max_retries()) { return false; } if ($task->get_current_try() >= $task->get_max_retries()) { return false; } $task->set_current_try($task->get_current_try() + 1); $this->failed_tasks[] = $task; return true; } /** * Schedules the cleanup task. * * @since 0.0.1 * @since 0.0.8 Updated to check if the Shepherd tables have been registered before scheduling the cleanup task. */ public function schedule_cleanup_task(): void { $prefix = Config::get_hook_prefix(); if (!did_action("shepherd_{$prefix}_tables_registered")) { return; } /** * Filters whether to schedule the cleanup task. * * @since 0.0.8 * * @param int $schedule_every_x_time The time in seconds to schedule the cleanup task. Default is 12 hours. */ $schedule_every_x_time = (int) apply_filters("shepherd_{$prefix}_schedule_cleanup_task_every", 12 * HOUR_IN_SECONDS); if (0 === $schedule_every_x_time) { return; } $this->dispatch(new Herding(), $schedule_every_x_time); /** * Fires when the cleanup task is scheduled. * * @since 0.0.8 */ do_action('shepherd_' . Config::get_hook_prefix() . '_cleanup_task_scheduled'); } /** * Reduce memory footprint by clearing the database query and object caches. * * @since 0.1.0 * * @return void */ private function free_memory(): void { /** * Globals. * * @var \wpdb $wpdb * @var WP_Object_Cache $wp_object_cache */ global $wpdb, $wp_object_cache; $wpdb->queries = []; if (!$wp_object_cache instanceof WP_Object_Cache) { return; } // Not all drop-ins support these props, however, there may be existing installations that rely on these being cleared. if (property_exists($wp_object_cache, 'group_ops')) { $wp_object_cache->group_ops = []; } if (property_exists($wp_object_cache, 'stats')) { $wp_object_cache->stats = []; } if (property_exists($wp_object_cache, 'memcache_debug')) { $wp_object_cache->memcache_debug = []; } if (property_exists($wp_object_cache, 'cache')) { $wp_object_cache->cache = []; } if (is_callable([$wp_object_cache, '__remoteset'])) { call_user_func([$wp_object_cache, '__remoteset']); // important! } } }