SchedulerTraits.php
367 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Traits for scheduling actions and dependencies. |
| 4 | */ |
| 5 | |
| 6 | namespace Automattic\WooCommerce\Admin\Schedulers; |
| 7 | |
| 8 | defined( 'ABSPATH' ) || exit; |
| 9 | |
| 10 | /** |
| 11 | * SchedulerTraits class. |
| 12 | */ |
| 13 | trait SchedulerTraits { |
| 14 | /** |
| 15 | * Action scheduler group. |
| 16 | * |
| 17 | * @var string|null |
| 18 | */ |
| 19 | public static $group = 'wc-admin-data'; |
| 20 | |
| 21 | /** |
| 22 | * Queue instance. |
| 23 | * |
| 24 | * @var WC_Queue_Interface |
| 25 | */ |
| 26 | protected static $queue = null; |
| 27 | |
| 28 | /** |
| 29 | * Add all actions as hooks. |
| 30 | */ |
| 31 | public static function init() { |
| 32 | foreach ( self::get_actions() as $action_name => $action_hook ) { |
| 33 | $method = new \ReflectionMethod( static::class, $action_name ); |
| 34 | add_action( $action_hook, array( static::class, 'do_action_or_reschedule' ), 10, $method->getNumberOfParameters() ); |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | /** |
| 39 | * Get queue instance. |
| 40 | * |
| 41 | * @return WC_Queue_Interface |
| 42 | */ |
| 43 | public static function queue() { |
| 44 | if ( is_null( self::$queue ) ) { |
| 45 | self::$queue = WC()->queue(); |
| 46 | } |
| 47 | |
| 48 | return self::$queue; |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * Set queue instance. |
| 53 | * |
| 54 | * @param WC_Queue_Interface $queue Queue instance. |
| 55 | */ |
| 56 | public static function set_queue( $queue ) { |
| 57 | self::$queue = $queue; |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * Gets the default scheduler actions for batching and scheduling actions. |
| 62 | */ |
| 63 | public static function get_default_scheduler_actions() { |
| 64 | return array( |
| 65 | 'schedule_action' => 'wc-admin_schedule_action_' . static::$name, |
| 66 | 'queue_batches' => 'wc-admin_queue_batches_' . static::$name, |
| 67 | ); |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Gets the actions for this specific scheduler. |
| 72 | * |
| 73 | * @return array |
| 74 | */ |
| 75 | public static function get_scheduler_actions() { |
| 76 | return array(); |
| 77 | } |
| 78 | |
| 79 | /** |
| 80 | * Get all available scheduling actions. |
| 81 | * Used to determine action hook names and clear events. |
| 82 | */ |
| 83 | public static function get_actions() { |
| 84 | return array_merge( |
| 85 | static::get_default_scheduler_actions(), |
| 86 | static::get_scheduler_actions() |
| 87 | ); |
| 88 | } |
| 89 | |
| 90 | /** |
| 91 | * Get an action tag name from the action name. |
| 92 | * |
| 93 | * @param string $action_name The action name. |
| 94 | * @return string|null |
| 95 | */ |
| 96 | public static function get_action( $action_name ) { |
| 97 | $actions = static::get_actions(); |
| 98 | return isset( $actions[ $action_name ] ) ? $actions[ $action_name ] : null; |
| 99 | } |
| 100 | |
| 101 | /** |
| 102 | * Returns an array of actions and dependencies as key => value pairs. |
| 103 | * |
| 104 | * @return array |
| 105 | */ |
| 106 | public static function get_dependencies() { |
| 107 | return array(); |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * Get dependencies associated with an action. |
| 112 | * |
| 113 | * @param string $action_name The action slug. |
| 114 | * @return string|null |
| 115 | */ |
| 116 | public static function get_dependency( $action_name ) { |
| 117 | $dependencies = static::get_dependencies(); |
| 118 | return isset( $dependencies[ $action_name ] ) ? $dependencies[ $action_name ] : null; |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * Batch action size. |
| 123 | */ |
| 124 | public static function get_batch_sizes() { |
| 125 | return array( |
| 126 | 'queue_batches' => 100, |
| 127 | ); |
| 128 | } |
| 129 | |
| 130 | /** |
| 131 | * Returns the batch size for an action. |
| 132 | * |
| 133 | * @param string $action Single batch action name. |
| 134 | * @return int Batch size. |
| 135 | */ |
| 136 | public static function get_batch_size( $action ) { |
| 137 | $batch_sizes = static::get_batch_sizes(); |
| 138 | $batch_size = isset( $batch_sizes[ $action ] ) ? $batch_sizes[ $action ] : 25; |
| 139 | |
| 140 | /** |
| 141 | * Filter the batch size for regenerating a report table. |
| 142 | * |
| 143 | * @param int $batch_size Batch size. |
| 144 | * @param string $action Batch action name. |
| 145 | */ |
| 146 | return apply_filters( 'woocommerce_analytics_regenerate_batch_size', $batch_size, static::$name, $action ); |
| 147 | } |
| 148 | |
| 149 | /** |
| 150 | * Flatten multidimensional arrays to store for scheduling. |
| 151 | * |
| 152 | * @param array $args Argument array. |
| 153 | * @return string |
| 154 | */ |
| 155 | public static function flatten_args( $args ) { |
| 156 | $flattened = array(); |
| 157 | |
| 158 | foreach ( $args as $arg ) { |
| 159 | if ( is_array( $arg ) ) { |
| 160 | $flattened[] = self::flatten_args( $arg ); |
| 161 | } else { |
| 162 | $flattened[] = $arg; |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | $string = '[' . implode( ',', $flattened ) . ']'; |
| 167 | return $string; |
| 168 | } |
| 169 | |
| 170 | /** |
| 171 | * Check if existing jobs exist for an action and arguments. |
| 172 | * |
| 173 | * @param string $action_name Action name. |
| 174 | * @param array $args Array of arguments to pass to action. |
| 175 | * @return bool |
| 176 | */ |
| 177 | public static function has_existing_jobs( $action_name, $args ) { |
| 178 | $existing_jobs = self::queue()->search( |
| 179 | array( |
| 180 | 'status' => 'pending', |
| 181 | 'per_page' => 1, |
| 182 | 'claimed' => false, |
| 183 | 'hook' => static::get_action( $action_name ), |
| 184 | 'search' => self::flatten_args( $args ), |
| 185 | 'group' => self::$group, |
| 186 | ) |
| 187 | ); |
| 188 | |
| 189 | if ( $existing_jobs ) { |
| 190 | $existing_job = current( $existing_jobs ); |
| 191 | |
| 192 | // Bail out if there's a pending single action, or a pending scheduled actions. |
| 193 | if ( |
| 194 | ( static::get_action( $action_name ) === $existing_job->get_hook() ) || |
| 195 | ( |
| 196 | static::get_action( 'schedule_action' ) === $existing_job->get_hook() && |
| 197 | in_array( self::get_action( $action_name ), $existing_job->get_args(), true ) |
| 198 | ) |
| 199 | ) { |
| 200 | return true; |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | return false; |
| 205 | } |
| 206 | |
| 207 | /** |
| 208 | * Get the next blocking job for an action. |
| 209 | * |
| 210 | * @param string $action_name Action name. |
| 211 | * @return false|ActionScheduler_Action |
| 212 | */ |
| 213 | public static function get_next_blocking_job( $action_name ) { |
| 214 | $dependency = self::get_dependency( $action_name ); |
| 215 | |
| 216 | if ( ! $dependency ) { |
| 217 | return false; |
| 218 | } |
| 219 | |
| 220 | $blocking_jobs = self::queue()->search( |
| 221 | array( |
| 222 | 'status' => 'pending', |
| 223 | 'orderby' => 'date', |
| 224 | 'order' => 'DESC', |
| 225 | 'per_page' => 1, |
| 226 | 'search' => $dependency, // search is used instead of hook to find queued batch creation. |
| 227 | 'group' => static::$group, |
| 228 | ) |
| 229 | ); |
| 230 | |
| 231 | return reset( $blocking_jobs ); |
| 232 | } |
| 233 | |
| 234 | /** |
| 235 | * Check for blocking jobs and reschedule if any exist. |
| 236 | */ |
| 237 | public static function do_action_or_reschedule() { |
| 238 | $action_hook = current_action(); |
| 239 | $action_name = array_search( $action_hook, static::get_actions(), true ); |
| 240 | $args = func_get_args(); |
| 241 | |
| 242 | // Check if any blocking jobs exist and schedule after they've completed |
| 243 | // or schedule to run now if no blocking jobs exist. |
| 244 | $blocking_job = static::get_next_blocking_job( $action_name ); |
| 245 | if ( $blocking_job ) { |
| 246 | $next_action_time = self::get_next_action_time( $blocking_job ); |
| 247 | |
| 248 | // Some actions, like single actions, don't have a next action time. |
| 249 | if ( ! is_a( $next_action_time, 'DateTime' ) ) { |
| 250 | $next_action_time = new \DateTime(); |
| 251 | } |
| 252 | |
| 253 | self::queue()->schedule_single( |
| 254 | $next_action_time->getTimestamp() + 5, |
| 255 | $action_hook, |
| 256 | $args, |
| 257 | static::$group |
| 258 | ); |
| 259 | } else { |
| 260 | call_user_func_array( array( static::class, $action_name ), $args ); |
| 261 | } |
| 262 | } |
| 263 | |
| 264 | /** |
| 265 | * Get the DateTime for the next scheduled time an action should run. |
| 266 | * This function allows backwards compatibility with Action Scheduler < v3.0. |
| 267 | * |
| 268 | * @param \ActionScheduler_Action $action Action. |
| 269 | * @return DateTime|null |
| 270 | */ |
| 271 | public static function get_next_action_time( $action ) { |
| 272 | if ( method_exists( $action->get_schedule(), 'get_next' ) ) { |
| 273 | $after = new \DateTime(); |
| 274 | $next_job_schedule = $action->get_schedule()->get_next( $after ); |
| 275 | } else { |
| 276 | $next_job_schedule = $action->get_schedule()->next(); |
| 277 | } |
| 278 | |
| 279 | return $next_job_schedule; |
| 280 | } |
| 281 | |
| 282 | /** |
| 283 | * Schedule an action to run and check for dependencies. |
| 284 | * |
| 285 | * @param string $action_name Action name. |
| 286 | * @param array $args Array of arguments to pass to action. |
| 287 | */ |
| 288 | public static function schedule_action( $action_name, $args = array() ) { |
| 289 | // Check for existing jobs and bail if they already exist. |
| 290 | if ( static::has_existing_jobs( $action_name, $args ) ) { |
| 291 | return; |
| 292 | } |
| 293 | |
| 294 | $action_hook = static::get_action( $action_name ); |
| 295 | if ( ! $action_hook ) { |
| 296 | return; |
| 297 | } |
| 298 | |
| 299 | if ( |
| 300 | // Skip scheduling if Action Scheduler tables have not been initialized. |
| 301 | ! get_option( 'schema-ActionScheduler_StoreSchema' ) || |
| 302 | apply_filters( 'woocommerce_analytics_disable_action_scheduling', false ) |
| 303 | ) { |
| 304 | call_user_func_array( array( static::class, $action_name ), $args ); |
| 305 | return; |
| 306 | } |
| 307 | |
| 308 | self::queue()->schedule_single( time() + 5, $action_hook, $args, static::$group ); |
| 309 | } |
| 310 | |
| 311 | /** |
| 312 | * Queue a large number of batch jobs, respecting the batch size limit. |
| 313 | * Reduces a range of batches down to "single batch" jobs. |
| 314 | * |
| 315 | * @param int $range_start Starting batch number. |
| 316 | * @param int $range_end Ending batch number. |
| 317 | * @param string $single_batch_action Action to schedule for a single batch. |
| 318 | * @param array $action_args Action arguments. |
| 319 | * @return void |
| 320 | */ |
| 321 | public static function queue_batches( $range_start, $range_end, $single_batch_action, $action_args = array() ) { |
| 322 | $batch_size = static::get_batch_size( 'queue_batches' ); |
| 323 | $range_size = 1 + ( $range_end - $range_start ); |
| 324 | $action_timestamp = time() + 5; |
| 325 | |
| 326 | if ( $range_size > $batch_size ) { |
| 327 | // If the current batch range is larger than a single batch, |
| 328 | // split the range into $queue_batch_size chunks. |
| 329 | $chunk_size = (int) ceil( $range_size / $batch_size ); |
| 330 | |
| 331 | for ( $i = 0; $i < $batch_size; $i++ ) { |
| 332 | $batch_start = (int) ( $range_start + ( $i * $chunk_size ) ); |
| 333 | $batch_end = (int) min( $range_end, $range_start + ( $chunk_size * ( $i + 1 ) ) - 1 ); |
| 334 | |
| 335 | if ( $batch_start > $range_end ) { |
| 336 | return; |
| 337 | } |
| 338 | |
| 339 | self::schedule_action( |
| 340 | 'queue_batches', |
| 341 | array( $batch_start, $batch_end, $single_batch_action, $action_args ) |
| 342 | ); |
| 343 | } |
| 344 | } else { |
| 345 | // Otherwise, queue the single batches. |
| 346 | for ( $i = $range_start; $i <= $range_end; $i++ ) { |
| 347 | $batch_action_args = array_merge( array( $i ), $action_args ); |
| 348 | self::schedule_action( $single_batch_action, $batch_action_args ); |
| 349 | } |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | /** |
| 354 | * Clears all queued actions. |
| 355 | */ |
| 356 | public static function clear_queued_actions() { |
| 357 | if ( version_compare( \ActionScheduler_Versions::instance()->latest_version(), '3.0', '>=' ) ) { |
| 358 | \ActionScheduler::store()->cancel_actions_by_group( static::$group ); |
| 359 | } else { |
| 360 | $actions = static::get_actions(); |
| 361 | foreach ( $actions as $action ) { |
| 362 | self::queue()->cancel_all( $action, null, static::$group ); |
| 363 | } |
| 364 | } |
| 365 | } |
| 366 | } |
| 367 |