├── .travis.yml ├── README.md ├── action-scheduler-custom-tables.php ├── codecov.yml ├── composer.json ├── src ├── DB_Logger.php ├── DB_Logger_Table_Maker.php ├── DB_Store.php ├── DB_Store_Migrator.php ├── DB_Store_Table_Maker.php ├── Dependencies.php ├── Hybrid_Store.php ├── Migration │ ├── Action_Migrator.php │ ├── Batch_Fetcher.php │ ├── Dry_Run_Action_Migrator.php │ ├── Dry_Run_Log_Migrator.php │ ├── Log_Migrator.php │ ├── Migration_Config.php │ ├── Migration_Runner.php │ └── Migration_Scheduler.php ├── Plugin.php ├── Table_Maker.php └── WP_CLI │ └── Migration_Command.php ├── tests ├── UnitTestCase.php ├── bootstrap.php ├── phpunit.xml.dist ├── phpunit │ ├── jobstore │ │ ├── DB_Store_Migrator_Test.php │ │ ├── DB_Store_Test.php │ │ └── Hybrid_Store_Test.php │ ├── logging │ │ └── DB_Logger_Test.php │ └── migration │ │ ├── Action_Migrator_Test.php │ │ ├── Batch_Fetcher_Test.php │ │ ├── Log_Migrator_Test.php │ │ ├── Migration_Config_Test.php │ │ ├── Migration_Runner_Test.php │ │ └── Migration_Scheduler_Test.php └── travis │ ├── setup.sh │ └── wp-tests-config.php └── vendor ├── autoload.php └── composer ├── ClassLoader.php ├── LICENSE ├── autoload_classmap.php ├── autoload_namespaces.php ├── autoload_psr4.php ├── autoload_real.php ├── autoload_static.php └── installed.json /.travis.yml: -------------------------------------------------------------------------------- 1 | # Travis CI Configuration File 2 | 3 | # Tell Travis CI we're using PHP 4 | language: php 5 | 6 | # Versions of PHP to test against 7 | php: 8 | - "5.6" 9 | - "7.0" 10 | - "7.1" 11 | 12 | # Specify versions of WordPress to test against 13 | # WP_VERSION = WordPress version number (use "master" for SVN trunk) 14 | # WP_MULTISITE = whether to test multisite (use either "0" or "1") 15 | # AS_VERSION = branch of action-scheduler to test against 16 | env: 17 | global: 18 | - AS_VERSION=version_3_0_0 19 | matrix: 20 | - WP_VERSION=4.9 WP_MULTISITE=0 21 | - WP_VERSION=4.8 WP_MULTISITE=0 22 | - WP_VERSION=4.7 WP_MULTISITE=0 23 | - WP_VERSION=4.6 WP_MULTISITE=0 24 | - WP_VERSION=4.5 WP_MULTISITE=0 25 | - WP_VERSION=4.4 WP_MULTISITE=0 26 | - WP_VERSION=4.9 WP_MULTISITE=1 27 | - WP_VERSION=4.8 WP_MULTISITE=1 28 | - WP_VERSION=4.7 WP_MULTISITE=1 29 | - WP_VERSION=4.6 WP_MULTISITE=1 30 | - WP_VERSION=4.5 WP_MULTISITE=1 31 | - WP_VERSION=4.4 WP_MULTISITE=1 32 | 33 | # Grab the setup script and execute 34 | before_script: 35 | - source tests/travis/setup.sh $TRAVIS_PHP_VERSION 36 | 37 | script: 38 | - if [[ "$TRAVIS_PHP_VERSION" == "7.1" ]] && [[ "$WP_VERSION" == "4.9" ]] && [[ "$WP_MULTISITE" == "0" ]] && [[ "$TRAVIS_BRANCH" == "master" ]]; then phpunit --configuration tests/phpunit.xml.dist --coverage-clover clover.xml; else phpunit --configuration tests/phpunit.xml.dist; fi 39 | 40 | after_script: 41 | - bash <(curl -s https://codecov.io/bash) 42 | 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Action Scheduler Custom Tables [![Build Status](https://travis-ci.org/Prospress/action-scheduler-custom-tables.png?branch=master)](https://travis-ci.org/Prospress/action-scheduler-custom-tables) [![codecov](https://codecov.io/gh/Prospress/action-scheduler-custom-tables/branch/master/graph/badge.svg)](https://codecov.io/gh/Prospress/action-scheduler-custom-tables) 2 | 3 | **This plugin is no longer needed.** 4 | 5 | The code in it is now part of [Action Scheduler version 3.0](https://github.com/woocommerce/action-scheduler) and newer. If you're using the latest Action Scheduler, you have the most performant schema available. 6 | 7 | This plugin was to prototype custom tables and test it on various large sites in advance of moving Action Scheduler core to custom tables. 8 | -------------------------------------------------------------------------------- /action-scheduler-custom-tables.php: -------------------------------------------------------------------------------- 1 | format( 'Y-m-d H:i:s' ); 29 | ActionScheduler_TimezoneHelper::set_local_timezone( $date ); 30 | $date_local = $date->format( 'Y-m-d H:i:s' ); 31 | 32 | /** @var \wpdb $wpdb */ 33 | global $wpdb; 34 | $wpdb->insert( $wpdb->actionscheduler_logs, [ 35 | 'action_id' => $action_id, 36 | 'message' => $message, 37 | 'log_date_gmt' => $date_gmt, 38 | 'log_date_local' => $date_local, 39 | ], [ '%d', '%s', '%s', '%s' ] ); 40 | 41 | return $wpdb->insert_id; 42 | } 43 | 44 | /** 45 | * @param string $entry_id 46 | * 47 | * @return ActionScheduler_LogEntry 48 | */ 49 | public function get_entry( $entry_id ) { 50 | /** @var \wpdb $wpdb */ 51 | global $wpdb; 52 | $entry = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE log_id=%d", $entry_id ) ); 53 | 54 | return $this->create_entry_from_db_record( $entry ); 55 | } 56 | 57 | /** 58 | * @param object $record 59 | * 60 | * @return ActionScheduler_LogEntry 61 | */ 62 | private function create_entry_from_db_record( $record ) { 63 | if ( empty( $record ) ) { 64 | return new ActionScheduler_NullLogEntry(); 65 | } 66 | 67 | $date = as_get_datetime_object( $record->log_date_gmt ); 68 | 69 | return new ActionScheduler_LogEntry( $record->action_id, $record->message, $date ); 70 | } 71 | 72 | /** 73 | * @param string $action_id 74 | * 75 | * @return ActionScheduler_LogEntry[] 76 | */ 77 | public function get_logs( $action_id ) { 78 | /** @var \wpdb $wpdb */ 79 | global $wpdb; 80 | 81 | $records = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE action_id=%d", $action_id ) ); 82 | 83 | return array_map( [ $this, 'create_entry_from_db_record' ], $records ); 84 | } 85 | 86 | /** 87 | * @codeCoverageIgnore 88 | */ 89 | public function init() { 90 | 91 | $table_maker = new DB_Logger_Table_Maker(); 92 | $table_maker->register_tables(); 93 | 94 | parent::init(); 95 | 96 | add_action( 'action_scheduler_deleted_action', [ $this, 'clear_deleted_action_logs' ], 10, 1 ); 97 | } 98 | 99 | public function clear_deleted_action_logs( $action_id ) { 100 | /** @var \wpdb $wpdb */ 101 | global $wpdb; 102 | $wpdb->delete( $wpdb->actionscheduler_logs, [ 'action_id' => $action_id, ], [ '%d' ] ); 103 | } 104 | 105 | } -------------------------------------------------------------------------------- /src/DB_Logger_Table_Maker.php: -------------------------------------------------------------------------------- 1 | tables = [ 19 | self::LOG_TABLE, 20 | ]; 21 | } 22 | 23 | protected function get_table_definition( $table ) { 24 | global $wpdb; 25 | $table_name = $wpdb->$table; 26 | $charset_collate = $wpdb->get_charset_collate(); 27 | $max_index_length = 191; // @see wp_get_db_schema() 28 | switch ( $table ) { 29 | 30 | case self::LOG_TABLE: 31 | 32 | return "CREATE TABLE {$table_name} ( 33 | log_id bigint(20) unsigned NOT NULL auto_increment, 34 | action_id bigint(20) unsigned NOT NULL, 35 | message text NOT NULL, 36 | log_date_gmt datetime NOT NULL default '0000-00-00 00:00:00', 37 | log_date_local datetime NOT NULL default '0000-00-00 00:00:00', 38 | PRIMARY KEY (log_id), 39 | KEY action_id (action_id), 40 | KEY log_date_gmt (log_date_gmt), 41 | KEY log_date_local (log_date_local) 42 | ) $charset_collate"; 43 | 44 | default: 45 | return ''; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/DB_Store.php: -------------------------------------------------------------------------------- 1 | register_tables(); 23 | } 24 | 25 | 26 | public function save_action( ActionScheduler_Action $action, \DateTime $date = null ) { 27 | try { 28 | /** @var \wpdb $wpdb */ 29 | global $wpdb; 30 | $data = [ 31 | 'hook' => $action->get_hook(), 32 | 'status' => ( $action->is_finished() ? self::STATUS_COMPLETE : self::STATUS_PENDING ), 33 | 'scheduled_date_gmt' => $this->get_scheduled_date_string( $action, $date ), 34 | 'scheduled_date_local' => $this->get_scheduled_date_string_local( $action, $date ), 35 | 'args' => json_encode( $action->get_args() ), 36 | 'schedule' => serialize( $action->get_schedule() ), 37 | 'group_id' => $this->get_group_id( $action->get_group() ), 38 | ]; 39 | $wpdb->insert( $wpdb->actionscheduler_actions, $data ); 40 | $action_id = $wpdb->insert_id; 41 | 42 | if ( is_wp_error( $action_id ) ) { 43 | throw new RuntimeException( $action_id->get_error_message() ); 44 | } 45 | elseif ( empty( $action_id ) ) { 46 | throw new RuntimeException( $wpdb->last_error ? $wpdb->last_error : __( 'Database error.', 'action-scheduler' ) ); 47 | } 48 | 49 | do_action( 'action_scheduler_stored_action', $action_id ); 50 | 51 | return $action_id; 52 | } catch ( \Exception $e ) { 53 | throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 ); 54 | } 55 | } 56 | 57 | /** 58 | * Get a group's ID based on its name/slug. 59 | * 60 | * @param string $slug The string name of a group. 61 | * @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group. 62 | * @return int The group's ID, if it exists or is created, or 0 if it does not exist and is not created. 63 | */ 64 | protected function get_group_id( $slug, $create_if_not_exists = true ) { 65 | if ( empty( $slug ) ) { 66 | return 0; 67 | } 68 | /** @var \wpdb $wpdb */ 69 | global $wpdb; 70 | $group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) ); 71 | if ( empty( $group_id ) && $create_if_not_exists ) { 72 | $group_id = $this->create_group( $slug ); 73 | } 74 | 75 | return $group_id; 76 | } 77 | 78 | protected function create_group( $slug ) { 79 | /** @var \wpdb $wpdb */ 80 | global $wpdb; 81 | $wpdb->insert( $wpdb->actionscheduler_groups, [ 'slug' => $slug ] ); 82 | 83 | return (int) $wpdb->insert_id; 84 | } 85 | 86 | public function fetch_action( $action_id ) { 87 | /** @var \wpdb $wpdb */ 88 | global $wpdb; 89 | $data = $wpdb->get_row( $wpdb->prepare( 90 | "SELECT a.*, g.slug AS `group` FROM {$wpdb->actionscheduler_actions} a LEFT JOIN {$wpdb->actionscheduler_groups} g ON a.group_id=g.group_id WHERE a.action_id=%d", 91 | $action_id 92 | ) ); 93 | 94 | if ( empty( $data ) ) { 95 | return $this->get_null_action(); 96 | } 97 | 98 | return $this->make_action_from_db_record( $data ); 99 | } 100 | 101 | protected function get_null_action() { 102 | return new ActionScheduler_NullAction(); 103 | } 104 | 105 | protected function make_action_from_db_record( $data ) { 106 | 107 | $hook = $data->hook; 108 | $args = json_decode( $data->args, true ); 109 | $schedule = unserialize( $data->schedule ); 110 | if ( empty( $schedule ) ) { 111 | $schedule = new ActionScheduler_NullSchedule(); 112 | } 113 | $group = $data->group ? $data->group : ''; 114 | if ( $data->status == self::STATUS_PENDING ) { 115 | $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); 116 | } elseif ( $data->status == self::STATUS_CANCELED ) { 117 | $action = new ActionScheduler_CanceledAction( $hook, $args, $schedule, $group ); 118 | } else { 119 | $action = new ActionScheduler_FinishedAction( $hook, $args, $schedule, $group ); 120 | } 121 | 122 | return $action; 123 | } 124 | 125 | /** 126 | * @param string $hook 127 | * @param array $params 128 | * 129 | * @return string ID of the next action matching the criteria or NULL if not found 130 | */ 131 | public function find_action( $hook, $params = [] ) { 132 | $params = wp_parse_args( $params, [ 133 | 'args' => null, 134 | 'status' => self::STATUS_PENDING, 135 | 'group' => '', 136 | ] ); 137 | 138 | /** @var wpdb $wpdb */ 139 | global $wpdb; 140 | $query = "SELECT a.action_id FROM {$wpdb->actionscheduler_actions} a"; 141 | $args = []; 142 | if ( ! empty( $params[ 'group' ] ) ) { 143 | $query .= " INNER JOIN {$wpdb->actionscheduler_groups} g ON g.group_id=a.group_id AND g.slug=%s"; 144 | $args[] = $params[ 'group' ]; 145 | } 146 | $query .= " WHERE a.hook=%s"; 147 | $args[] = $hook; 148 | if ( ! is_null( $params[ 'args' ] ) ) { 149 | $query .= " AND a.args=%s"; 150 | $args[] = json_encode( $params[ 'args' ] ); 151 | } 152 | 153 | $order = 'ASC'; 154 | if ( ! empty( $params[ 'status' ] ) ) { 155 | $query .= " AND a.status=%s"; 156 | $args[] = $params[ 'status' ]; 157 | 158 | if ( self::STATUS_PENDING == $params[ 'status' ] ) { 159 | $order = 'ASC'; // Find the next action that matches 160 | } else { 161 | $order = 'DESC'; // Find the most recent action that matches 162 | } 163 | } 164 | 165 | $query .= " ORDER BY scheduled_date_gmt $order LIMIT 1"; 166 | 167 | $query = $wpdb->prepare( $query, $args ); 168 | 169 | $id = $wpdb->get_var( $query ); 170 | 171 | return $id; 172 | } 173 | 174 | /** 175 | * Returns the SQL statement to query (or count) actions. 176 | * 177 | * @param array $query Filtering options 178 | * @param string $select_or_count Whether the SQL should select and return the IDs or just the row count 179 | * 180 | * @return string SQL statement. The returned SQL is already properly escaped. 181 | */ 182 | protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) { 183 | 184 | if ( ! in_array( $select_or_count, array( 'select', 'count' ) ) ) { 185 | throw new \InvalidArgumentException( __( 'Invalid valud for select or count parameter. Cannot query actions.', 'action-scheduler' ) ); 186 | } 187 | 188 | $query = wp_parse_args( $query, [ 189 | 'hook' => '', 190 | 'args' => null, 191 | 'date' => null, 192 | 'date_compare' => '<=', 193 | 'modified' => null, 194 | 'modified_compare' => '<=', 195 | 'group' => '', 196 | 'status' => '', 197 | 'claimed' => null, 198 | 'per_page' => 5, 199 | 'offset' => 0, 200 | 'orderby' => 'date', 201 | 'order' => 'ASC', 202 | ] ); 203 | 204 | /** @var \wpdb $wpdb */ 205 | global $wpdb; 206 | $sql = ( 'count' === $select_or_count ) ? 'SELECT count(a.action_id)' : 'SELECT a.action_id'; 207 | $sql .= " FROM {$wpdb->actionscheduler_actions} a"; 208 | $sql_params = []; 209 | 210 | if ( ! empty( $query[ 'group' ] ) || 'group' === $query[ 'orderby' ] ) { 211 | $sql .= " LEFT JOIN {$wpdb->actionscheduler_groups} g ON g.group_id=a.group_id"; 212 | } 213 | 214 | $sql .= " WHERE 1=1"; 215 | 216 | if ( ! empty( $query[ 'group' ] ) ) { 217 | $sql .= " AND g.slug=%s"; 218 | $sql_params[] = $query[ 'group' ]; 219 | } 220 | 221 | if ( $query[ 'hook' ] ) { 222 | $sql .= " AND a.hook=%s"; 223 | $sql_params[] = $query[ 'hook' ]; 224 | } 225 | if ( ! is_null( $query[ 'args' ] ) ) { 226 | $sql .= " AND a.args=%s"; 227 | $sql_params[] = json_encode( $query[ 'args' ] ); 228 | } 229 | 230 | if ( $query[ 'status' ] ) { 231 | $sql .= " AND a.status=%s"; 232 | $sql_params[] = $query[ 'status' ]; 233 | } 234 | 235 | if ( $query[ 'date' ] instanceof \DateTime ) { 236 | $date = clone $query[ 'date' ]; 237 | $date->setTimezone( new \DateTimeZone( 'UTC' ) ); 238 | $date_string = $date->format( 'Y-m-d H:i:s' ); 239 | $comparator = $this->validate_sql_comparator( $query[ 'date_compare' ] ); 240 | $sql .= " AND a.scheduled_date_gmt $comparator %s"; 241 | $sql_params[] = $date_string; 242 | } 243 | 244 | if ( $query[ 'modified' ] instanceof \DateTime ) { 245 | $modified = clone $query[ 'modified' ]; 246 | $modified->setTimezone( new \DateTimeZone( 'UTC' ) ); 247 | $date_string = $modified->format( 'Y-m-d H:i:s' ); 248 | $comparator = $this->validate_sql_comparator( $query[ 'modified_compare' ] ); 249 | $sql .= " AND a.last_attempt_gmt $comparator %s"; 250 | $sql_params[] = $date_string; 251 | } 252 | 253 | if ( $query[ 'claimed' ] === true ) { 254 | $sql .= " AND a.claim_id != 0"; 255 | } elseif ( $query[ 'claimed' ] === false ) { 256 | $sql .= " AND a.claim_id = 0"; 257 | } elseif ( ! is_null( $query[ 'claimed' ] ) ) { 258 | $sql .= " AND a.claim_id = %d"; 259 | $sql_params[] = $query[ 'claimed' ]; 260 | } 261 | 262 | if ( ! empty( $query['search'] ) ) { 263 | $sql .= " AND (a.hook LIKE %s OR a.args LIKE %s"; 264 | for( $i = 0; $i < 2; $i++ ) { 265 | $sql_params[] = sprintf( '%%%s%%', $query['search'] ); 266 | } 267 | 268 | $search_claim_id = (int) $query['search']; 269 | if ( $search_claim_id ) { 270 | $sql .= ' OR a.claim_id = %d'; 271 | $sql_params[] = $search_claim_id; 272 | } 273 | 274 | $sql .= ')'; 275 | } 276 | 277 | if ( 'select' === $select_or_count ) { 278 | switch ( $query['orderby'] ) { 279 | case 'hook': 280 | $orderby = 'a.hook'; 281 | break; 282 | case 'group': 283 | $orderby = 'g.slug'; 284 | break; 285 | case 'modified': 286 | $orderby = 'a.last_attempt_gmt'; 287 | break; 288 | case 'date': 289 | default: 290 | $orderby = 'a.scheduled_date_gmt'; 291 | break; 292 | } 293 | if ( strtoupper( $query[ 'order' ] ) == 'ASC' ) { 294 | $order = 'ASC'; 295 | } else { 296 | $order = 'DESC'; 297 | } 298 | $sql .= " ORDER BY $orderby $order"; 299 | if ( $query[ 'per_page' ] > 0 ) { 300 | $sql .= " LIMIT %d, %d"; 301 | $sql_params[] = $query[ 'offset' ]; 302 | $sql_params[] = $query[ 'per_page' ]; 303 | } 304 | } 305 | 306 | if ( ! empty( $sql_params ) ) { 307 | $sql = $wpdb->prepare( $sql, $sql_params ); 308 | } 309 | 310 | return $sql; 311 | } 312 | 313 | /** 314 | * @param array $query 315 | * @param string $query_type Whether to select or count the results. Default, select. 316 | * @return null|string|array The IDs of actions matching the query 317 | */ 318 | public function query_actions( $query = [], $query_type = 'select' ) { 319 | /** @var wpdb $wpdb */ 320 | global $wpdb; 321 | 322 | $sql = $this->get_query_actions_sql( $query, $query_type ); 323 | 324 | return ( 'count' === $query_type ) ? $wpdb->get_var( $sql ) : $wpdb->get_col( $sql ); 325 | } 326 | 327 | /** 328 | * Get a count of all actions in the store, grouped by status 329 | * 330 | * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status. 331 | */ 332 | public function action_counts() { 333 | global $wpdb; 334 | 335 | $sql = "SELECT a.status, count(a.status) as 'count'"; 336 | $sql .= " FROM {$wpdb->actionscheduler_actions} a"; 337 | $sql .= " GROUP BY a.status"; 338 | 339 | $actions_count_by_status = array(); 340 | $action_stati_and_labels = $this->get_status_labels(); 341 | 342 | foreach ( $wpdb->get_results( $sql ) as $action_data ) { 343 | // Ignore any actions with invalid status 344 | if ( array_key_exists( $action_data->status, $action_stati_and_labels ) ) { 345 | $actions_count_by_status[ $action_data->status ] = $action_data->count; 346 | } 347 | } 348 | 349 | return $actions_count_by_status; 350 | } 351 | 352 | /** 353 | * @param string $action_id 354 | * 355 | * @throws \InvalidArgumentException 356 | * @return void 357 | */ 358 | public function cancel_action( $action_id ) { 359 | /** @var \wpdb $wpdb */ 360 | global $wpdb; 361 | $updated = $wpdb->update( 362 | $wpdb->actionscheduler_actions, 363 | [ 'status' => self::STATUS_CANCELED ], 364 | [ 'action_id' => $action_id ], 365 | [ '%s' ], 366 | [ '%d' ] 367 | ); 368 | if ( empty( $updated ) ) { 369 | throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); 370 | } 371 | do_action( 'action_scheduler_canceled_action', $action_id ); 372 | } 373 | 374 | 375 | public function delete_action( $action_id ) { 376 | /** @var \wpdb $wpdb */ 377 | global $wpdb; 378 | $deleted = $wpdb->delete( $wpdb->actionscheduler_actions, [ 'action_id' => $action_id ], [ '%d' ] ); 379 | if ( empty( $deleted ) ) { 380 | throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); 381 | } 382 | do_action( 'action_scheduler_deleted_action', $action_id ); 383 | } 384 | 385 | /** 386 | * @param string $action_id 387 | * 388 | * @throws \InvalidArgumentException 389 | * @return \DateTime The local date the action is scheduled to run, or the date that it ran. 390 | */ 391 | public function get_date( $action_id ) { 392 | $date = $this->get_date_gmt( $action_id ); 393 | ActionScheduler_TimezoneHelper::set_local_timezone( $date ); 394 | return $date; 395 | } 396 | 397 | /** 398 | * @param string $action_id 399 | * 400 | * @throws \InvalidArgumentException 401 | * @return \DateTime The GMT date the action is scheduled to run, or the date that it ran. 402 | */ 403 | protected function get_date_gmt( $action_id ) { 404 | /** @var \wpdb $wpdb */ 405 | global $wpdb; 406 | $record = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d", $action_id ) ); 407 | if ( empty( $record ) ) { 408 | throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); 409 | } 410 | if ( $record->status == self::STATUS_PENDING ) { 411 | return as_get_datetime_object( $record->scheduled_date_gmt ); 412 | } else { 413 | return as_get_datetime_object( $record->last_attempt_gmt ); 414 | } 415 | } 416 | 417 | 418 | /** 419 | * @param int $max_actions 420 | * @param \DateTime $before_date Jobs must be schedule before this date. Defaults to now. 421 | * 422 | * @return ActionScheduler_ActionClaim 423 | */ 424 | public function stake_claim( $max_actions = 10, \DateTime $before_date = null, $hooks = array(), $group = '' ) { 425 | $claim_id = $this->generate_claim_id(); 426 | $this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group ); 427 | $action_ids = $this->find_actions_by_claim_id( $claim_id ); 428 | 429 | return new ActionScheduler_ActionClaim( $claim_id, $action_ids ); 430 | } 431 | 432 | 433 | protected function generate_claim_id() { 434 | /** @var \wpdb $wpdb */ 435 | global $wpdb; 436 | $now = as_get_datetime_object(); 437 | $wpdb->insert( $wpdb->actionscheduler_claims, [ 'date_created_gmt' => $now->format( 'Y-m-d H:i:s' ) ] ); 438 | 439 | return $wpdb->insert_id; 440 | } 441 | 442 | /** 443 | * @param string $claim_id 444 | * @param int $limit 445 | * @param \DateTime $before_date Should use UTC timezone. 446 | * 447 | * @return int The number of actions that were claimed 448 | * @throws \RuntimeException 449 | */ 450 | protected function claim_actions( $claim_id, $limit, \DateTime $before_date = null, $hooks = array(), $group = '' ) { 451 | /** @var \wpdb $wpdb */ 452 | global $wpdb; 453 | 454 | $now = as_get_datetime_object(); 455 | $date = is_null( $before_date ) ? $now : clone $before_date; 456 | 457 | // can't use $wpdb->update() because of the <= condition 458 | $update = "UPDATE {$wpdb->actionscheduler_actions} SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s"; 459 | $params = array( 460 | $claim_id, 461 | $now->format( 'Y-m-d H:i:s' ), 462 | current_time( 'mysql' ), 463 | ); 464 | 465 | $where = "WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s"; 466 | $params[] = $date->format( 'Y-m-d H:i:s' ); 467 | $params[] = self::STATUS_PENDING; 468 | 469 | if ( ! empty( $hooks ) ) { 470 | $placeholders = array_fill( 0, count( $hooks ), '%s' ); 471 | $where .= ' AND hook IN (' . join( ', ', $placeholders ) . ')'; 472 | $params = array_merge( $params, array_values( $hooks ) ); 473 | } 474 | 475 | if ( ! empty( $group ) ) { 476 | 477 | $group_id = $this->get_group_id( $group, false ); 478 | 479 | // throw exception if no matching group found, this matches ActionScheduler_wpPostStore's behaviour 480 | if ( empty( $group_id ) ) { 481 | throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'action-scheduler' ), $group ) ); 482 | } 483 | 484 | $where .= ' AND group_id = %d'; 485 | $params[] = $group_id; 486 | } 487 | 488 | $order = "ORDER BY attempts ASC, scheduled_date_gmt ASC, action_id ASC LIMIT %d"; 489 | $params[] = $limit; 490 | 491 | $sql = $wpdb->prepare( "{$update} {$where} {$order}", $params ); 492 | 493 | $rows_affected = $wpdb->query( $sql ); 494 | if ( $rows_affected === false ) { 495 | throw new \RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) ); 496 | } 497 | 498 | return (int) $rows_affected; 499 | } 500 | 501 | /** 502 | * @return int 503 | */ 504 | public function get_claim_count() { 505 | global $wpdb; 506 | 507 | $sql = "SELECT COUNT(DISTINCT claim_id) FROM {$wpdb->actionscheduler_actions} WHERE claim_id != 0 AND status IN ( %s, %s)"; 508 | $sql = $wpdb->prepare( $sql, [ self::STATUS_PENDING, self::STATUS_RUNNING ] ); 509 | 510 | return (int) $wpdb->get_var( $sql ); 511 | } 512 | 513 | /** 514 | * Return an action's claim ID, as stored in the claim_id column 515 | * 516 | * @param string $action_id 517 | * @return mixed 518 | */ 519 | public function get_claim_id( $action_id ) { 520 | /** @var \wpdb $wpdb */ 521 | global $wpdb; 522 | 523 | $sql = "SELECT claim_id FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d"; 524 | $sql = $wpdb->prepare( $sql, $action_id ); 525 | 526 | return (int) $wpdb->get_var( $sql ); 527 | } 528 | 529 | /** 530 | * @param string $claim_id 531 | * 532 | * @return int[] 533 | */ 534 | public function find_actions_by_claim_id( $claim_id ) { 535 | /** @var \wpdb $wpdb */ 536 | global $wpdb; 537 | 538 | $sql = "SELECT action_id FROM {$wpdb->actionscheduler_actions} WHERE claim_id=%d"; 539 | $sql = $wpdb->prepare( $sql, $claim_id ); 540 | 541 | $action_ids = $wpdb->get_col( $sql ); 542 | 543 | return array_map( 'intval', $action_ids ); 544 | } 545 | 546 | public function release_claim( ActionScheduler_ActionClaim $claim ) { 547 | /** @var \wpdb $wpdb */ 548 | global $wpdb; 549 | $wpdb->update( $wpdb->actionscheduler_actions, [ 'claim_id' => 0 ], [ 'claim_id' => $claim->get_id() ], [ '%d' ], [ '%d' ] ); 550 | $wpdb->delete( $wpdb->actionscheduler_claims, [ 'claim_id' => $claim->get_id() ], [ '%d' ] ); 551 | } 552 | 553 | /** 554 | * @param string $action_id 555 | * 556 | * @return void 557 | */ 558 | public function unclaim_action( $action_id ) { 559 | /** @var \wpdb $wpdb */ 560 | global $wpdb; 561 | $wpdb->update( 562 | $wpdb->actionscheduler_actions, 563 | [ 'claim_id' => 0 ], 564 | [ 'action_id' => $action_id ], 565 | [ '%s' ], 566 | [ '%d' ] 567 | ); 568 | } 569 | 570 | public function mark_failure( $action_id ) { 571 | /** @var \wpdb $wpdb */ 572 | global $wpdb; 573 | $updated = $wpdb->update( 574 | $wpdb->actionscheduler_actions, 575 | [ 'status' => self::STATUS_FAILED ], 576 | [ 'action_id' => $action_id ], 577 | [ '%s' ], 578 | [ '%d' ] 579 | ); 580 | if ( empty( $updated ) ) { 581 | throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); 582 | } 583 | } 584 | 585 | /** 586 | * @param string $action_id 587 | * 588 | * @return void 589 | */ 590 | public function log_execution( $action_id ) { 591 | /** @var \wpdb $wpdb */ 592 | global $wpdb; 593 | 594 | $sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d"; 595 | $sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id ); 596 | $wpdb->query( $sql ); 597 | } 598 | 599 | 600 | public function mark_complete( $action_id ) { 601 | /** @var \wpdb $wpdb */ 602 | global $wpdb; 603 | $updated = $wpdb->update( 604 | $wpdb->actionscheduler_actions, 605 | [ 606 | 'status' => self::STATUS_COMPLETE, 607 | 'last_attempt_gmt' => current_time( 'mysql', true ), 608 | 'last_attempt_local' => current_time( 'mysql' ), 609 | ], 610 | [ 'action_id' => $action_id ], 611 | [ '%s' ], 612 | [ '%d' ] 613 | ); 614 | if ( empty( $updated ) ) { 615 | throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'action-scheduler' ), $action_id ) ); 616 | } 617 | } 618 | 619 | public function get_status( $action_id ) { 620 | /** @var \wpdb $wpdb */ 621 | global $wpdb; 622 | $sql = "SELECT status FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d"; 623 | $sql = $wpdb->prepare( $sql, $action_id ); 624 | $status = $wpdb->get_var( $sql ); 625 | 626 | if ( $status === null ) { 627 | throw new \InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) ); 628 | } elseif ( empty( $status ) ) { 629 | throw new \RuntimeException( __( 'Unknown status found for action.', 'action-scheduler' ) ); 630 | } else { 631 | return $status; 632 | } 633 | } 634 | 635 | 636 | } 637 | -------------------------------------------------------------------------------- /src/DB_Store_Migrator.php: -------------------------------------------------------------------------------- 1 | $this->get_scheduled_date_string( $action, $last_attempt_date ), 33 | 'last_attempt_local' => $this->get_scheduled_date_string_local( $action, $last_attempt_date ), 34 | ]; 35 | 36 | $wpdb->update( $wpdb->actionscheduler_actions, $data, array( 'action_id' => $action_id ), array( '%s', '%s' ), array( '%d' ) ); 37 | } 38 | 39 | return $action_id; 40 | } catch ( \Exception $e ) { 41 | throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 ); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/DB_Store_Table_Maker.php: -------------------------------------------------------------------------------- 1 | tables = [ 21 | self::ACTIONS_TABLE, 22 | self::CLAIMS_TABLE, 23 | self::GROUPS_TABLE, 24 | ]; 25 | } 26 | 27 | protected function get_table_definition( $table ) { 28 | global $wpdb; 29 | $table_name = $wpdb->$table; 30 | $charset_collate = $wpdb->get_charset_collate(); 31 | $max_index_length = 191; // @see wp_get_db_schema() 32 | switch ( $table ) { 33 | 34 | case self::ACTIONS_TABLE: 35 | 36 | return "CREATE TABLE {$table_name} ( 37 | action_id bigint(20) unsigned NOT NULL auto_increment, 38 | hook varchar(191) NOT NULL, 39 | status varchar(20) NOT NULL, 40 | scheduled_date_gmt datetime NOT NULL default '0000-00-00 00:00:00', 41 | scheduled_date_local datetime NOT NULL default '0000-00-00 00:00:00', 42 | args varchar($max_index_length), 43 | schedule longtext, 44 | group_id bigint(20) unsigned NOT NULL default '0', 45 | attempts int(11) NOT NULL default '0', 46 | last_attempt_gmt datetime NOT NULL default '0000-00-00 00:00:00', 47 | last_attempt_local datetime NOT NULL default '0000-00-00 00:00:00', 48 | claim_id bigint(20) unsigned NOT NULL default '0', 49 | PRIMARY KEY (action_id), 50 | KEY hook (hook($max_index_length)), 51 | KEY status (status), 52 | KEY scheduled_date_gmt (scheduled_date_gmt), 53 | KEY scheduled_date_local (scheduled_date_local), 54 | KEY args (args($max_index_length)), 55 | KEY group_id (group_id), 56 | KEY last_attempt_gmt (last_attempt_gmt), 57 | KEY last_attempt_local (last_attempt_local), 58 | KEY claim_id (claim_id) 59 | ) $charset_collate"; 60 | 61 | case self::CLAIMS_TABLE: 62 | 63 | return "CREATE TABLE {$table_name} ( 64 | claim_id bigint(20) unsigned NOT NULL auto_increment, 65 | date_created_gmt datetime NOT NULL default '0000-00-00 00:00:00', 66 | PRIMARY KEY (claim_id), 67 | KEY date_created_gmt (date_created_gmt) 68 | ) $charset_collate"; 69 | 70 | case self::GROUPS_TABLE: 71 | 72 | return "CREATE TABLE {$table_name} ( 73 | group_id bigint(20) unsigned NOT NULL auto_increment, 74 | slug varchar(255) NOT NULL, 75 | PRIMARY KEY (group_id), 76 | KEY slug (slug($max_index_length)) 77 | ) $charset_collate"; 78 | 79 | default: 80 | return ''; 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/Dependencies.php: -------------------------------------------------------------------------------- 1 | is_action_scheduler_available() && $this->is_action_scheduler_new_enough() && $this->is_action_scheduler_old_enough(); 30 | } 31 | 32 | /** 33 | * Attach an admin notice in the WordPress admin about the missing dependeices (if any) 34 | */ 35 | public function add_notice() { 36 | add_filter( 'admin_notices', [ $this, 'admin_notices' ], 10, 1 ); 37 | } 38 | 39 | /** 40 | * Display an inactive notice when Action Scheduler is inactive or an invalid version is running. 41 | */ 42 | public function admin_notices() { 43 | if ( ! $this->dependencies_met() && current_user_can( 'activate_plugins' ) ) : ?> 44 |
45 |

46 | tags 48 | printf( esc_html__( '%1$sThe Action Scheduler Custom Tables plugin is not running.%2$s', 'action-scheduler' ), '', '' ); 49 | 50 | $plugins_url = admin_url( 'plugins.php' ); 51 | 52 | if ( ! $this->is_action_scheduler_available() ) { 53 | // translators: 1$-2$: link tags to the Action Scheduler GitHub page, 3$-4$: link tags for Plugins administration screen 54 | printf( esc_html__( 'The %1$sAction Scheduler library%2$s must be active for this plugin to work. No instance of Action Scheduler was detected. Please %3$sinstall & activate Action Scheduler as a plugin »%4$s', 'action-scheduler' ), '', '', '', '' ); 55 | } elseif ( ! $this->is_action_scheduler_new_enough() ) { 56 | // translators: 1$-2$: link tags to the Action Scheduler GitHub page, 3$-4$: version number, e.g. 2.1.0 5$-6$: link tags for Plugins administration screen 57 | printf( esc_html__( 'The %1$sAction Scheduler library%2$s version %3$s or newer must be active for this plugin to work. Action Scheduler version %4$s was detected. Please %5$supdate Action Scheduler as a plugin »%6$s', 'action-scheduler' ), '', '', $this->min_version, $this->get_action_scheduler_version(), '', '' ); 58 | } elseif ( ! $this->is_action_scheduler_old_enough() ) { 59 | // translators: 1$-2$: link tags to the Action Scheduler GitHub page, 3$-4$: version number, e.g. 2.1.0 5$-6$: link tags for Plugins administration screen 60 | printf( esc_html__( 'This plugin is only required with %1$sAction Scheduler%2$s versions prior to %3$s. Action Scheduler version %4$s was detected. Please disable and delete the Action Scheduler Custom Tables plugin on the %5$sPlugins Administration screen »%6$s', 'action-scheduler' ), '', '', $this->max_version, $this->get_action_scheduler_version(), '', '' ); 61 | } 62 | ?> 63 |

64 |
65 | get_action_scheduler_version(), $this->min_version, '>=' ); 84 | } 85 | 86 | /** 87 | * Action Scheduler v3.0.0 will include custom tables in core making this plugin unnecessary. 88 | * 89 | * @return bool 90 | */ 91 | private function is_action_scheduler_old_enough() { 92 | return version_compare( $this->get_action_scheduler_version(), $this->max_version, '<' ); 93 | } 94 | 95 | /** 96 | * Check Action Scheduler class is available and at required versions. 97 | * 98 | * @return string 99 | */ 100 | private function get_action_scheduler_version() { 101 | return $this->is_action_scheduler_available() ? \ActionScheduler_Versions::instance()->latest_version() : '0.0.0'; 102 | } 103 | } -------------------------------------------------------------------------------- /src/Hybrid_Store.php: -------------------------------------------------------------------------------- 1 | demarkation_id = (int) get_option( self::DEMARKATION_OPTION, 0 ); 41 | if ( empty( $config ) ) { 42 | $config = Plugin::instance()->get_migration_config_object(); 43 | } 44 | $this->primary_store = $config->get_destination_store(); 45 | $this->secondary_store = $config->get_source_store(); 46 | $this->migration_runner = new Migration_Runner( $config ); 47 | } 48 | 49 | /** 50 | * @codeCoverageIgnore 51 | */ 52 | public function init() { 53 | add_action( 'action_scheduler/custom_tables/created_table', [ $this, 'set_autoincrement' ], 10, 2 ); 54 | $this->primary_store->init(); 55 | $this->secondary_store->init(); 56 | remove_action( 'action_scheduler/custom_tables/created_table', [ $this, 'set_autoincrement' ], 10 ); 57 | } 58 | 59 | /** 60 | * When the actions table is created, set its autoincrement 61 | * value to be one higher than the posts table to ensure that 62 | * there are no ID collisions. 63 | * 64 | * @param string $table_name 65 | * @param string $table_suffix 66 | * 67 | * @return void 68 | * @codeCoverageIgnore 69 | */ 70 | public function set_autoincrement( $table_name, $table_suffix ) { 71 | if ( DB_Store_Table_Maker::ACTIONS_TABLE === $table_suffix ) { 72 | if ( empty( $this->demarkation_id ) ) { 73 | $this->demarkation_id = $this->set_demarkation_id(); 74 | } 75 | /** @var \wpdb $wpdb */ 76 | global $wpdb; 77 | $wpdb->insert( 78 | $wpdb->{DB_Store_Table_Maker::ACTIONS_TABLE}, 79 | [ 80 | 'action_id' => $this->demarkation_id, 81 | 'hook' => '', 82 | 'status' => '', 83 | ] 84 | ); 85 | $wpdb->delete( 86 | $wpdb->{DB_Store_Table_Maker::ACTIONS_TABLE}, 87 | [ 'action_id' => $this->demarkation_id ] 88 | ); 89 | } 90 | } 91 | 92 | /** 93 | * @param int $id The ID to set as the demarkation point between the two stores 94 | * Leave null to use the next ID from the WP posts table. 95 | * 96 | * @return int The new ID. 97 | * 98 | * @codeCoverageIgnore 99 | */ 100 | private function set_demarkation_id( $id = null ) { 101 | if ( empty( $id ) ) { 102 | /** @var \wpdb $wpdb */ 103 | global $wpdb; 104 | $id = (int) $wpdb->get_var( "SELECT MAX(ID) FROM $wpdb->posts" ); 105 | $id ++; 106 | } 107 | update_option( self::DEMARKATION_OPTION, $id ); 108 | 109 | return $id; 110 | } 111 | 112 | /** 113 | * Find the first matching action from the secondary store. 114 | * If it exists, migrate it to the primary store immediately. 115 | * After it migrates, the secondary store will logically contain 116 | * the next matching action, so return the result thence. 117 | * 118 | * @param string $hook 119 | * @param array $params 120 | * 121 | * @return string 122 | */ 123 | public function find_action( $hook, $params = [] ) { 124 | $found_unmigrated_action = $this->secondary_store->find_action( $hook, $params ); 125 | if ( ! empty( $found_unmigrated_action ) ) { 126 | $this->migrate( [ $found_unmigrated_action ] ); 127 | } 128 | 129 | return $this->primary_store->find_action( $hook, $params ); 130 | } 131 | 132 | /** 133 | * Find actions matching the query in the secondary source first. 134 | * If any are found, migrate them immediately. Then the secondary 135 | * store will contain the canonical results. 136 | * 137 | * @param array $query 138 | * @param string $query_type Whether to select or count the results. Default, select. 139 | * 140 | * @return int[] 141 | */ 142 | public function query_actions( $query = [], $query_type = 'select' ) { 143 | $found_unmigrated_actions = $this->secondary_store->query_actions( $query, 'select' ); 144 | if ( ! empty( $found_unmigrated_actions ) ) { 145 | $this->migrate( $found_unmigrated_actions ); 146 | } 147 | 148 | return $this->primary_store->query_actions( $query, $query_type ); 149 | } 150 | 151 | /** 152 | * Get a count of all actions in the store, grouped by status 153 | * 154 | * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status. 155 | */ 156 | public function action_counts() { 157 | $unmigrated_actions_count = $this->secondary_store->action_counts(); 158 | $migrated_actions_count = $this->primary_store->action_counts(); 159 | $actions_count_by_status = array(); 160 | 161 | foreach ( $this->get_status_labels() as $status_key => $status_label ) { 162 | 163 | $count = 0; 164 | 165 | if ( isset( $unmigrated_actions_count[ $status_key ] ) ) { 166 | $count += $unmigrated_actions_count[ $status_key ]; 167 | } 168 | 169 | if ( isset( $migrated_actions_count[ $status_key ] ) ) { 170 | $count += $migrated_actions_count[ $status_key ]; 171 | } 172 | 173 | $actions_count_by_status[ $status_key ] = $count; 174 | } 175 | 176 | $actions_count_by_status = array_filter( $actions_count_by_status ); 177 | 178 | return $actions_count_by_status; 179 | } 180 | 181 | /** 182 | * If any actions would have been claimed by the secondary store, 183 | * migrate them immediately, then ask the primary store for the 184 | * canonical claim. 185 | * 186 | * @param int $max_actions 187 | * @param DateTime|null $before_date 188 | * 189 | * @return ActionScheduler_ActionClaim 190 | */ 191 | public function stake_claim( $max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '' ) { 192 | $claim = $this->secondary_store->stake_claim( $max_actions, $before_date, $hooks, $group ); 193 | 194 | $claimed_actions = $claim->get_actions(); 195 | if ( ! empty( $claimed_actions ) ) { 196 | $this->migrate( $claimed_actions ); 197 | } 198 | 199 | $this->secondary_store->release_claim( $claim ); 200 | 201 | return $this->primary_store->stake_claim( $max_actions, $before_date, $hooks, $group ); 202 | } 203 | 204 | private function migrate( $action_ids ) { 205 | $this->migration_runner->migrate_actions( $action_ids ); 206 | } 207 | 208 | public function save_action( ActionScheduler_Action $action, DateTime $date = null ) { 209 | return $this->primary_store->save_action( $action, $date ); 210 | } 211 | 212 | public function fetch_action( $action_id ) { 213 | if ( $action_id < $this->demarkation_id ) { 214 | return $this->secondary_store->fetch_action( $action_id ); 215 | } else { 216 | return $this->primary_store->fetch_action( $action_id ); 217 | } 218 | } 219 | 220 | public function cancel_action( $action_id ) { 221 | if ( $action_id < $this->demarkation_id ) { 222 | $this->secondary_store->cancel_action( $action_id ); 223 | } else { 224 | $this->primary_store->cancel_action( $action_id ); 225 | } 226 | } 227 | 228 | public function delete_action( $action_id ) { 229 | if ( $action_id < $this->demarkation_id ) { 230 | $this->secondary_store->delete_action( $action_id ); 231 | } else { 232 | $this->primary_store->delete_action( $action_id ); 233 | } 234 | } 235 | 236 | public function get_date( $action_id ) { 237 | if ( $action_id < $this->demarkation_id ) { 238 | return $this->secondary_store->get_date( $action_id ); 239 | } else { 240 | return $this->primary_store->get_date( $action_id ); 241 | } 242 | } 243 | 244 | public function mark_failure( $action_id ) { 245 | if ( $action_id < $this->demarkation_id ) { 246 | $this->secondary_store->mark_failure( $action_id ); 247 | } else { 248 | $this->primary_store->mark_failure( $action_id ); 249 | } 250 | } 251 | 252 | public function log_execution( $action_id ) { 253 | if ( $action_id < $this->demarkation_id ) { 254 | $this->secondary_store->log_execution( $action_id ); 255 | } else { 256 | $this->primary_store->log_execution( $action_id ); 257 | } 258 | } 259 | 260 | public function mark_complete( $action_id ) { 261 | if ( $action_id < $this->demarkation_id ) { 262 | $this->secondary_store->mark_complete( $action_id ); 263 | } else { 264 | $this->primary_store->mark_complete( $action_id ); 265 | } 266 | } 267 | 268 | public function get_status( $action_id ) { 269 | if ( $action_id < $this->demarkation_id ) { 270 | return $this->secondary_store->get_status( $action_id ); 271 | } else { 272 | return $this->primary_store->get_status( $action_id ); 273 | } 274 | } 275 | 276 | 277 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * 278 | * All claim-related functions should operate solely 279 | * on the primary store. 280 | * * * * * * * * * * * * * * * * * * * * * * * * * * */ 281 | 282 | public function get_claim_count() { 283 | return $this->primary_store->get_claim_count(); 284 | } 285 | 286 | public function get_claim_id( $action_id ) { 287 | return $this->primary_store->get_claim_id( $action_id ); 288 | } 289 | 290 | public function release_claim( ActionScheduler_ActionClaim $claim ) { 291 | $this->primary_store->release_claim( $claim ); 292 | } 293 | 294 | public function unclaim_action( $action_id ) { 295 | $this->primary_store->unclaim_action( $action_id ); 296 | } 297 | 298 | public function find_actions_by_claim_id( $claim_id ) { 299 | return $this->primary_store->find_actions_by_claim_id( $claim_id ); 300 | } 301 | } -------------------------------------------------------------------------------- /src/Migration/Action_Migrator.php: -------------------------------------------------------------------------------- 1 | source = $source_store; 14 | $this->destination = $destination_store; 15 | $this->log_migrator = $log_migrator; 16 | } 17 | 18 | public function migrate( $source_action_id ) { 19 | $action = $this->source->fetch_action( $source_action_id ); 20 | 21 | try { 22 | $status = $this->source->get_status( $source_action_id ); 23 | } catch ( \Exception $e ) { 24 | $status = ''; 25 | } 26 | 27 | if ( empty( $status ) || ! $action->get_schedule()->next() ) { 28 | // empty status means the action didn't exist 29 | // null schedule means it's missing vital data 30 | // delete it and move on 31 | try { 32 | $this->source->delete_action( $source_action_id ); 33 | } catch ( \Exception $e ) { 34 | // nothing to do, it didn't exist in the first place 35 | } 36 | do_action( 'action_scheduler/custom_tables/no_action_to_migrate', $source_action_id, $this->source, $this->destination ); 37 | 38 | return 0; 39 | } 40 | 41 | try { 42 | 43 | // Make sure the last attempt date is set correctly for completed and failed actions 44 | $last_attempt_date = ( $status !== \ActionScheduler_Store::STATUS_PENDING ) ? $this->source->get_date( $source_action_id ) : null; 45 | 46 | $destination_action_id = $this->destination->save_action( $action, null, $last_attempt_date ); 47 | } catch ( \Exception $e ) { 48 | do_action( 'action_scheduler/custom_tables/migrate_action_failed', $source_action_id, $this->source, $this->destination ); 49 | 50 | return 0; // could not save the action in the new store 51 | } 52 | 53 | 54 | try { 55 | if ( $status === \ActionScheduler_Store::STATUS_FAILED ) { 56 | $this->destination->mark_failure( $destination_action_id ); 57 | } 58 | 59 | $this->log_migrator->migrate( $source_action_id, $destination_action_id ); 60 | $this->source->delete_action( $source_action_id ); 61 | 62 | do_action( 'action_scheduler/custom_tables/migrated_action', $source_action_id, $destination_action_id, $this->source, $this->destination ); 63 | 64 | return $destination_action_id; 65 | } catch ( \Exception $e ) { 66 | // could not delete from the old store 67 | do_action( 'action_scheduler/custom_tables/migrate_action_incomplete', $source_action_id, $destination_action_id, $this->source, $this->destination ); 68 | do_action( 'action_scheduler/custom_tables/migrated_action', $source_action_id, $destination_action_id, $this->source, $this->destination ); 69 | 70 | return $destination_action_id; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/Migration/Batch_Fetcher.php: -------------------------------------------------------------------------------- 1 | store = $source_store; 14 | } 15 | 16 | /** 17 | * @param int $count The number of actions to retrieve 18 | * 19 | * @return int[] A list of action IDs 20 | */ 21 | public function fetch( $count = 10 ) { 22 | foreach ( $this->get_query_strategies( $count ) as $query ) { 23 | $action_ids = $this->store->query_actions( $query ); 24 | if ( ! empty( $action_ids ) ) { 25 | return $action_ids; 26 | } 27 | } 28 | 29 | return []; 30 | } 31 | 32 | private function get_query_strategies( $count ) { 33 | $now = as_get_datetime_object(); 34 | $args = [ 35 | 'date' => $now, 36 | 'per_page' => $count, 37 | 'offset' => 0, 38 | 'orderby' => 'date', 39 | 'order' => 'ASC', 40 | ]; 41 | 42 | $priorities = [ 43 | Store::STATUS_PENDING, 44 | Store::STATUS_FAILED, 45 | Store::STATUS_CANCELED, 46 | Store::STATUS_COMPLETE, 47 | Store::STATUS_RUNNING, 48 | '', // any other unanticipated status 49 | ]; 50 | 51 | foreach ( $priorities as $status ) { 52 | yield wp_parse_args( [ 53 | 'status' => $status, 54 | 'date_compare' => '<=', 55 | ], $args ); 56 | yield wp_parse_args( [ 57 | 'status' => $status, 58 | 'date_compare' => '>=', 59 | ], $args ); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/Migration/Dry_Run_Action_Migrator.php: -------------------------------------------------------------------------------- 1 | source = $source_logger; 15 | $this->destination = $destination_Logger; 16 | } 17 | 18 | public function migrate( $source_action_id, $destination_action_id ) { 19 | $logs = $this->source->get_logs( $source_action_id ); 20 | foreach ( $logs as $log ) { 21 | if ( $log->get_action_id() == $source_action_id ) { 22 | $this->destination->log( $destination_action_id, $log->get_message(), $log->get_date() ); 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/Migration/Migration_Config.php: -------------------------------------------------------------------------------- 1 | source_store ) ) { 36 | throw new \RuntimeException( __( 'Source store must be configured before running a migration', 'action-scheduler' ) ); 37 | } 38 | 39 | return $this->source_store; 40 | } 41 | 42 | /** 43 | * @param Store $store 44 | */ 45 | public function set_source_store( Store $store ) { 46 | $this->source_store = $store; 47 | } 48 | 49 | /** 50 | * @return Logger 51 | */ 52 | public function get_source_logger() { 53 | if ( empty( $this->source_logger ) ) { 54 | throw new \RuntimeException( __( 'Source logger must be configured before running a migration', 'action-scheduler' ) ); 55 | } 56 | 57 | return $this->source_logger; 58 | } 59 | 60 | /** 61 | * @param Logger $logger 62 | */ 63 | public function set_source_logger( Logger $logger ) { 64 | $this->source_logger = $logger; 65 | } 66 | 67 | /** 68 | * @return Store 69 | */ 70 | public function get_destination_store() { 71 | if ( empty( $this->destination_store ) ) { 72 | throw new \RuntimeException( __( 'Destination store must be configured before running a migration', 'action-scheduler' ) ); 73 | } 74 | 75 | return $this->destination_store; 76 | } 77 | 78 | /** 79 | * @param Store $store 80 | */ 81 | public function set_destination_store( Store $store ) { 82 | $this->destination_store = $store; 83 | } 84 | 85 | /** 86 | * @return Logger 87 | */ 88 | public function get_destination_logger() { 89 | if ( empty( $this->destination_logger ) ) { 90 | throw new \RuntimeException( __( 'Destination logger must be configured before running a migration', 'action-scheduler' ) ); 91 | } 92 | 93 | return $this->destination_logger; 94 | } 95 | 96 | /** 97 | * @param Logger $logger 98 | */ 99 | public function set_destination_logger( Logger $logger ) { 100 | $this->destination_logger = $logger; 101 | } 102 | 103 | /** 104 | * @return bool 105 | */ 106 | public function get_dry_run() { 107 | return $this->dry_run; 108 | } 109 | 110 | /** 111 | * @param bool $dry_run 112 | */ 113 | public function set_dry_run( $dry_run ) { 114 | $this->dry_run = (bool) $dry_run; 115 | } 116 | 117 | } -------------------------------------------------------------------------------- /src/Migration/Migration_Runner.php: -------------------------------------------------------------------------------- 1 | source_store = $config->get_source_store(); 18 | $this->destination_store = $config->get_destination_store(); 19 | $this->source_logger = $config->get_source_logger(); 20 | $this->destination_logger = $config->get_destination_logger(); 21 | 22 | $this->batch_fetcher = new Batch_Fetcher( $this->source_store ); 23 | if ( $config->get_dry_run() ) { 24 | $this->log_migrator = new Dry_Run_Log_Migrator( $this->source_logger, $this->destination_logger ); 25 | $this->action_migrator = new Dry_Run_Action_Migrator( $this->source_store, $this->destination_store, $this->log_migrator ); 26 | } else { 27 | $this->log_migrator = new Log_Migrator( $this->source_logger, $this->destination_logger ); 28 | $this->action_migrator = new Action_Migrator( $this->source_store, $this->destination_store, $this->log_migrator ); 29 | } 30 | } 31 | 32 | public function run( $batch_size = 10 ) { 33 | $batch = $this->batch_fetcher->fetch( $batch_size ); 34 | 35 | $this->migrate_actions( $batch ); 36 | 37 | return count( $batch ); 38 | } 39 | 40 | public function migrate_actions( array $action_ids ) { 41 | do_action( 'action_scheduler/custom_tables/migration_batch_starting', $action_ids ); 42 | 43 | remove_action( 'action_scheduler_stored_action', array( \ActionScheduler::logger(), 'log_stored_action' ), 10 ); 44 | remove_action( 'action_scheduler_stored_action', array( $this->destination_logger, 'log_stored_action' ), 10 ); 45 | 46 | foreach ( $action_ids as $source_action_id ) { 47 | $destination_action_id = $this->action_migrator->migrate( $source_action_id ); 48 | if ( $destination_action_id ) { 49 | $this->destination_logger->log( $destination_action_id, sprintf( 50 | __( 'Migrated action with ID %d in %s to ID %d in %s', 'action-scheduler' ), 51 | $source_action_id, 52 | get_class( $this->source_store ), 53 | $destination_action_id, 54 | get_class( $this->destination_store ) 55 | ) ); 56 | } 57 | } 58 | 59 | add_action( 'action_scheduler_stored_action', array( \ActionScheduler::logger(), 'log_stored_action' ), 10 , 1 ); 60 | add_action( 'action_scheduler_stored_action', array( $this->destination_logger, 'log_stored_action' ), 10, 1 ); 61 | 62 | do_action( 'action_scheduler/custom_tables/migration_batch_complete', $action_ids ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Migration/Migration_Scheduler.php: -------------------------------------------------------------------------------- 1 | get_migration_runner(); 34 | $count = $migration_runner->run( $this->get_batch_size() ); 35 | if ( $count === 0 ) { 36 | $this->mark_complete(); 37 | } else { 38 | $this->schedule_migration( time() + $this->get_schedule_interval() ); 39 | } 40 | } 41 | 42 | public function mark_complete() { 43 | $this->unschedule_migration(); 44 | update_option( self::STATUS_FLAG, self::STATUS_COMPLETE ); 45 | do_action( 'action_scheduler/custom_tables/migration_complete' ); 46 | } 47 | 48 | /** 49 | * @return bool Whether the flag has been set marking the migration as complete 50 | */ 51 | public function is_migration_complete() { 52 | return get_option( self::STATUS_FLAG ) === self::STATUS_COMPLETE; 53 | } 54 | 55 | /** 56 | * @return bool Whether there is a pending action in the store to handle the migration 57 | */ 58 | public function is_migration_scheduled() { 59 | $next = as_next_scheduled_action( self::HOOK ); 60 | 61 | return ! empty( $next ); 62 | } 63 | 64 | /** 65 | * @param int $when Timestamp to run the next migration batch. Defaults to now. 66 | * 67 | * @return string The action ID 68 | */ 69 | public function schedule_migration( $when = 0 ) { 70 | if ( empty( $when ) ) { 71 | $when = time(); 72 | } 73 | 74 | return as_schedule_single_action( $when, self::HOOK, [], self::GROUP ); 75 | } 76 | 77 | /** 78 | * Removes the scheduled migration action 79 | */ 80 | public function unschedule_migration() { 81 | as_unschedule_action( self::HOOK, null, self::GROUP ); 82 | } 83 | 84 | /** 85 | * @return int Seconds between migration runs. Defaults to two minutes. 86 | */ 87 | private function get_schedule_interval() { 88 | return (int) apply_filters( 'action_scheduler/custom_tables/migration_interval', 2 * MINUTE_IN_SECONDS ); 89 | } 90 | 91 | /** 92 | * @return int Number of actions to migrate in each batch. Defaults to 1000. 93 | */ 94 | private function get_batch_size() { 95 | return (int) apply_filters( 'action_scheduler/custom_tables/migration_batch_size', 1000 ); 96 | } 97 | 98 | /** 99 | * @return Migration_Runner 100 | */ 101 | private function get_migration_runner() { 102 | $config = Plugin::instance()->get_migration_config_object(); 103 | 104 | return new Migration_Runner( $config ); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | migration_scheduler = $migration_scheduler; 36 | self::$dependency_handler = $dependency_handler; 37 | } 38 | 39 | /** 40 | * Override the action store with our own 41 | * 42 | * @param string $class 43 | * 44 | * @return string 45 | */ 46 | public function set_store_class( $class ) { 47 | if ( $this->migration_scheduler->is_migration_complete() ) { 48 | return DB_Store::class; 49 | } else { 50 | return Hybrid_Store::class; 51 | } 52 | } 53 | 54 | /** 55 | * Override the logger with our own 56 | * 57 | * @param string $class 58 | * 59 | * @return string 60 | */ 61 | public function set_logger_class( $class ) { 62 | return DB_Logger::class; 63 | } 64 | 65 | /** 66 | * Register the WP-CLI command to handle bulk migrations 67 | * 68 | * @return void 69 | */ 70 | public function register_cli_command() { 71 | if ( class_exists( 'WP_CLI_Command' ) ) { 72 | $command = new WP_CLI\Migration_Command(); 73 | $command->register(); 74 | } 75 | } 76 | 77 | /** 78 | * Set up the background migration process 79 | * 80 | * @return void 81 | */ 82 | public function schedule_migration() { 83 | if ( ! self::$dependency_handler->dependencies_met() || $this->migration_scheduler->is_migration_complete() || $this->migration_scheduler->is_migration_scheduled() ) { 84 | return; 85 | } 86 | 87 | $this->migration_scheduler->schedule_migration(); 88 | } 89 | 90 | /** 91 | * Get the default migration config object 92 | * 93 | * @return Migration\Migration_Config 94 | */ 95 | public function get_migration_config_object() { 96 | $config = new Migration\Migration_Config(); 97 | $config->set_source_store( new \ActionScheduler_wpPostStore() ); 98 | $config->set_source_logger( new \ActionScheduler_wpCommentLogger() ); 99 | $config->set_destination_store( new DB_Store_Migrator() ); 100 | $config->set_destination_logger( new DB_Logger() ); 101 | 102 | return apply_filters( 'action_scheduler/custom_tables/migration_config', $config ); 103 | } 104 | 105 | public function hook_admin_notices() { 106 | if ( $this->migration_scheduler->is_migration_complete() ) { 107 | return; 108 | } 109 | add_action( 'admin_notices', [ $this, 'display_migration_notice' ], 10, 0 ); 110 | } 111 | 112 | public function display_migration_notice() { 113 | printf( '

%s

', __( 'Action Scheduler migration in progress. The list of scheduled actions may be incomplete.' ) ); 114 | } 115 | 116 | private function hook() { 117 | 118 | add_filter( 'action_scheduler_store_class', [ $this, 'set_store_class' ], 10, 1 ); 119 | add_filter( 'action_scheduler_logger_class', [ $this, 'set_logger_class' ], 10, 1 ); 120 | add_action( 'plugins_loaded', [ $this, 'register_cli_command' ], 10, 0 ); 121 | add_action( 'init', [ $this, 'maybe_hook_migration' ] ); 122 | add_action( 'shutdown', [ $this, 'schedule_migration' ], 0, 0 ); 123 | 124 | // Action Scheduler may be displayed as a Tools screen or WooCommerce > Status administration screen 125 | add_action( 'load-tools_page_action-scheduler', [ $this, 'hook_admin_notices' ], 10, 0 ); 126 | add_action( 'load-woocommerce_page_wc-status', [ $this, 'hook_admin_notices' ], 10, 0 ); 127 | } 128 | 129 | /** 130 | * Possibly hook the migration scheduler action. 131 | * 132 | * @author Jeremy Pry 133 | */ 134 | public function maybe_hook_migration() { 135 | if ( $this->migration_scheduler->is_migration_complete() ) { 136 | return; 137 | } 138 | 139 | $this->migration_scheduler->hook(); 140 | } 141 | 142 | public static function init() { 143 | 144 | self::instance(); 145 | 146 | if ( self::$dependency_handler->dependencies_met() ) { 147 | self::instance()->hook(); 148 | } else { 149 | self::$dependency_handler->add_notice(); 150 | } 151 | } 152 | 153 | public static function instance() { 154 | if ( ! isset( self::$instance ) ) { 155 | self::$instance = new static( new Migration_Scheduler(), new Dependencies() ); 156 | } 157 | 158 | return self::$instance; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Table_Maker.php: -------------------------------------------------------------------------------- 1 | tables as $table ) { 37 | $wpdb->tables[] = $table; 38 | $name = $this->get_full_table_name( $table ); 39 | $wpdb->$table = $name; 40 | } 41 | 42 | // create the tables 43 | if ( $this->schema_update_required() ) { 44 | foreach ( $this->tables as $table ) { 45 | $this->update_table( $table ); 46 | } 47 | $this->mark_schema_update_complete(); 48 | } 49 | } 50 | 51 | /** 52 | * @param string $table The name of the table 53 | * 54 | * @return string The CREATE TABLE statement, suitable for passing to dbDelta 55 | */ 56 | abstract protected function get_table_definition( $table ); 57 | 58 | /** 59 | * Determine if the database schema is out of date 60 | * by comparing the integer found in $this->schema_version 61 | * with the option set in the WordPress options table 62 | * 63 | * @return bool 64 | */ 65 | private function schema_update_required() { 66 | $option_name = 'schema-' . static::class; 67 | $version_found_in_db = get_option( $option_name, 0 ); 68 | 69 | return version_compare( $version_found_in_db, $this->schema_version, '<' ); 70 | } 71 | 72 | /** 73 | * Update the option in WordPress to indicate that 74 | * our schema is now up to date 75 | * 76 | * @return void 77 | */ 78 | private function mark_schema_update_complete() { 79 | $option_name = 'schema-' . static::class; 80 | 81 | // work around race conditions and ensure that our option updates 82 | $value_to_save = (string) $this->schema_version . '.0.' . time(); 83 | 84 | update_option( $option_name, $value_to_save ); 85 | } 86 | 87 | /** 88 | * Update the schema for the given table 89 | * 90 | * @param string $table The name of the table to update 91 | * 92 | * @return void 93 | */ 94 | private function update_table( $table ) { 95 | require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); 96 | $definition = $this->get_table_definition( $table ); 97 | if ( $definition ) { 98 | $updated = dbDelta( $definition ); 99 | foreach ( $updated as $updated_table => $update_description ) { 100 | if ( strpos( $update_description, 'Created table' ) === 0 ) { 101 | do_action( 'action_scheduler/custom_tables/created_table', $updated_table, $table ); 102 | } 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * @param string $table 109 | * 110 | * @return string The full name of the table, including the 111 | * table prefix for the current blog 112 | */ 113 | protected function get_full_table_name( $table ) { 114 | return $GLOBALS[ 'wpdb' ]->prefix . $table; 115 | } 116 | } -------------------------------------------------------------------------------- /src/WP_CLI/Migration_Command.php: -------------------------------------------------------------------------------- 1 | 'Migrates actions to the custom tables store', 33 | 'synopsis' => [ 34 | [ 35 | 'type' => 'assoc', 36 | 'name' => 'batch-size', 37 | 'optional' => true, 38 | 'default' => 100, 39 | 'description' => 'The number of actions to process in each batch', 40 | ], 41 | [ 42 | 'type' => 'flag', 43 | 'name' => 'dry-run', 44 | 'optional' => true, 45 | 'description' => 'Reports on the actions that would have been migrated, but does not change any data', 46 | ], 47 | ], 48 | ] ); 49 | } 50 | 51 | /** 52 | * @param array $positional_args 53 | * @param array $assoc_args 54 | * 55 | * @return void 56 | */ 57 | public function migrate( $positional_args, $assoc_args ) { 58 | if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) { 59 | return; 60 | } 61 | 62 | $this->init_logging(); 63 | 64 | $config = $this->get_migration_config( $assoc_args ); 65 | 66 | $runner = new Migration_Runner( $config ); 67 | $batch_size = isset( $assoc_args[ 'batch-size' ] ) ? (int) $assoc_args[ 'batch-size' ] : 100; 68 | 69 | do { 70 | $actions_processed = $runner->run( $batch_size ); 71 | $this->total_processed += $actions_processed; 72 | } while ( $actions_processed > 0 ); 73 | 74 | if ( ! $config->get_dry_run() ) { 75 | // let the scheduler know that there's nothing left to do 76 | $scheduler = new Migration_Scheduler(); 77 | $scheduler->mark_complete(); 78 | } 79 | 80 | WP_CLI::success( sprintf( '%s complete. %d actions processed.', $config->get_dry_run() ? 'Dry run' : 'Migration', $this->total_processed ) ); 81 | } 82 | 83 | /** 84 | * Build the config object used to create the Migration_Runner 85 | * 86 | * @param array $args 87 | * 88 | * @return Migration_Config 89 | */ 90 | private function get_migration_config( $args ) { 91 | $args = wp_parse_args( $args, [ 92 | 'dry-run' => false, 93 | ] ); 94 | 95 | $config = Plugin::instance()->get_migration_config_object(); 96 | $config->set_dry_run( ! empty( $args[ 'dry-run' ] ) ); 97 | 98 | return $config; 99 | } 100 | 101 | private function init_logging() { 102 | add_action( 'action_scheduler/custom_tables/migrate_action_dry_run', function ( $action_id ) { 103 | WP_CLI::debug( sprintf( 'Dry-run: migrated action %d', $action_id ) ); 104 | }, 10, 1 ); 105 | add_action( 'action_scheduler/custom_tables/no_action_to_migrate', function ( $action_id ) { 106 | WP_CLI::debug( sprintf( 'No action found to migrate for ID %d', $action_id ) ); 107 | }, 10, 1 ); 108 | add_action( 'action_scheduler/custom_tables/migrate_action_failed', function ( $action_id ) { 109 | WP_CLI::warning( sprintf( 'Failed migrating action with ID %d', $action_id ) ); 110 | }, 10, 1 ); 111 | add_action( 'action_scheduler/custom_tables/migrate_action_incomplete', function ( $source_id, $destination_id ) { 112 | WP_CLI::warning( sprintf( 'Unable to remove source action with ID %d after migrating to new ID %d', $source_id, $destination_id ) ); 113 | }, 10, 2 ); 114 | add_action( 'action_scheduler/custom_tables/migrated_action', function ( $source_id, $destination_id ) { 115 | WP_CLI::debug( sprintf( 'Migrated source action with ID %d to new store with ID %d', $source_id, $destination_id ) ); 116 | }, 10, 2 ); 117 | add_action( 'action_scheduler/custom_tables/migration_batch_starting', function ( $batch ) { 118 | WP_CLI::debug( 'Beginning migration of batch: ' . print_r( $batch, true ) ); 119 | }, 10, 1 ); 120 | add_action( 'action_scheduler/custom_tables/migration_batch_complete', function ( $batch ) { 121 | WP_CLI::log( sprintf( 'Completed migration of %d actions', count( $batch ) ) ); 122 | }, 10, 1 ); 123 | } 124 | } -------------------------------------------------------------------------------- /tests/UnitTestCase.php: -------------------------------------------------------------------------------- 1 | createResult(); 35 | } 36 | 37 | if ( 'UTC' != ( $this->existing_timezone = date_default_timezone_get() ) ) { 38 | date_default_timezone_set( 'UTC' ); 39 | $result->run( $this ); 40 | } 41 | 42 | date_default_timezone_set( 'Pacific/Fiji' ); // UTC+12 43 | $result->run( $this ); 44 | 45 | date_default_timezone_set( 'Pacific/Tahiti' ); // UTC-10: it's a magical place 46 | $result->run( $this ); 47 | 48 | date_default_timezone_set( $this->existing_timezone ); 49 | 50 | return $result; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./phpunit 17 | 18 | 19 | 20 | ../../action-scheduler/tests/phpunit 21 | 22 | ../../action-scheduler/tests/phpunit/jobstore/ActionScheduler_wpPostStore_Test.php 23 | ../../action-scheduler/tests/phpunit/logging/ActionScheduler_wpCommentLogger_Test.php 24 | 25 | 26 | 27 | 28 | ignore 29 | 30 | 31 | 32 | 33 | ../src 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/phpunit/jobstore/DB_Store_Migrator_Test.php: -------------------------------------------------------------------------------- 1 | save_action( $action, null, $last_attempt_date ); 23 | $action_date = $store->get_date( $action_id ); 24 | 25 | $this->assertEquals( $last_attempt_date->format( 'U' ), $action_date->format( 'U' ) ); 26 | 27 | $action_id = $store->save_action( $action, $scheduled_date, $last_attempt_date ); 28 | $action_date = $store->get_date( $action_id ); 29 | 30 | $this->assertEquals( $last_attempt_date->format( 'U' ), $action_date->format( 'U' ) ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/phpunit/jobstore/DB_Store_Test.php: -------------------------------------------------------------------------------- 1 | save_action( $action ); 22 | 23 | $this->assertNotEmpty( $action_id ); 24 | } 25 | 26 | public function test_create_action_with_scheduled_date() { 27 | $time = as_get_datetime_object( strtotime( '-1 week' ) ); 28 | $action = new ActionScheduler_Action( 'my_hook', [], new ActionScheduler_SimpleSchedule( $time ) ); 29 | $store = new DB_Store(); 30 | $action_id = $store->save_action( $action, $time ); 31 | $action_date = $store->get_date( $action_id ); 32 | 33 | $this->assertEquals( $time->format( 'U' ), $action_date->format( 'U' ) ); 34 | } 35 | 36 | public function test_retrieve_action() { 37 | $time = as_get_datetime_object(); 38 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 39 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule, 'my_group' ); 40 | $store = new DB_Store(); 41 | $action_id = $store->save_action( $action ); 42 | 43 | $retrieved = $store->fetch_action( $action_id ); 44 | $this->assertEquals( $action->get_hook(), $retrieved->get_hook() ); 45 | $this->assertEqualSets( $action->get_args(), $retrieved->get_args() ); 46 | $this->assertEquals( $action->get_schedule()->next()->format( 'U' ), $retrieved->get_schedule()->next()->format( 'U' ) ); 47 | $this->assertEquals( $action->get_group(), $retrieved->get_group() ); 48 | } 49 | 50 | public function test_cancel_action() { 51 | $time = as_get_datetime_object(); 52 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 53 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule, 'my_group' ); 54 | $store = new DB_Store(); 55 | $action_id = $store->save_action( $action ); 56 | $store->cancel_action( $action_id ); 57 | 58 | $fetched = $store->fetch_action( $action_id ); 59 | $this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched ); 60 | } 61 | 62 | public function test_claim_actions() { 63 | $created_actions = []; 64 | $store = new DB_Store(); 65 | for ( $i = 3; $i > - 3; $i -- ) { 66 | $time = as_get_datetime_object( $i . ' hours' ); 67 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 68 | $action = new ActionScheduler_Action( 'my_hook', [ $i ], $schedule, 'my_group' ); 69 | 70 | $created_actions[] = $store->save_action( $action ); 71 | } 72 | 73 | $claim = $store->stake_claim(); 74 | $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); 75 | 76 | $this->assertCount( 3, $claim->get_actions() ); 77 | $this->assertEqualSets( array_slice( $created_actions, 3, 3 ), $claim->get_actions() ); 78 | } 79 | 80 | public function test_claim_actions_order() { 81 | 82 | $store = new DB_Store(); 83 | $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) ); 84 | $created_actions = array( 85 | $store->save_action( new ActionScheduler_Action( 'my_hook', array( 1 ), $schedule, 'my_group' ) ), 86 | $store->save_action( new ActionScheduler_Action( 'my_hook', array( 1 ), $schedule, 'my_group' ) ), 87 | ); 88 | 89 | $claim = $store->stake_claim(); 90 | $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); 91 | 92 | // Verify uniqueness of action IDs. 93 | $this->assertEquals( 2, count( array_unique( $created_actions ) ) ); 94 | 95 | // Verify the count and order of the actions. 96 | $claimed_actions = $claim->get_actions(); 97 | $this->assertCount( 2, $claimed_actions ); 98 | $this->assertEquals( $created_actions, $claimed_actions ); 99 | 100 | // Verify the reversed order doesn't pass. 101 | $reversed_actions = array_reverse( $created_actions ); 102 | $this->assertNotEquals( $reversed_actions, $claimed_actions ); 103 | } 104 | 105 | public function test_claim_actions_by_hooks() { 106 | $created_actions = $created_actions_by_hook = []; 107 | $store = new DB_Store(); 108 | $unique_hook_one = 'my_unique_hook_one'; 109 | $unique_hook_two = 'my_unique_hook_two'; 110 | $unique_hooks = array( 111 | $unique_hook_one, 112 | $unique_hook_two, 113 | ); 114 | 115 | for ( $i = 3; $i > - 3; $i -- ) { 116 | foreach ( $unique_hooks as $unique_hook ) { 117 | $time = as_get_datetime_object( $i . ' hours' ); 118 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 119 | $action = new ActionScheduler_Action( $unique_hook, [ $i ], $schedule, 'my_group' ); 120 | 121 | $action_id = $store->save_action( $action ); 122 | $created_actions[] = $created_actions_by_hook[ $unique_hook ][] = $action_id; 123 | } 124 | } 125 | 126 | $claim = $store->stake_claim( 10, null, $unique_hooks ); 127 | $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); 128 | $this->assertCount( 6, $claim->get_actions() ); 129 | $this->assertEqualSets( array_slice( $created_actions, 6, 6 ), $claim->get_actions() ); 130 | 131 | $store->release_claim( $claim ); 132 | 133 | $claim = $store->stake_claim( 10, null, array( $unique_hook_one ) ); 134 | $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); 135 | $this->assertCount( 3, $claim->get_actions() ); 136 | $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_one ], 3, 3 ), $claim->get_actions() ); 137 | 138 | $store->release_claim( $claim ); 139 | 140 | $claim = $store->stake_claim( 10, null, array( $unique_hook_two ) ); 141 | $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); 142 | $this->assertCount( 3, $claim->get_actions() ); 143 | $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_two ], 3, 3 ), $claim->get_actions() ); 144 | } 145 | 146 | public function test_claim_actions_by_group() { 147 | $created_actions = []; 148 | $store = new DB_Store(); 149 | $unique_group_one = 'my_unique_group_one'; 150 | $unique_group_two = 'my_unique_group_two'; 151 | $unique_groups = array( 152 | $unique_group_one, 153 | $unique_group_two, 154 | ); 155 | 156 | for ( $i = 3; $i > - 3; $i -- ) { 157 | foreach ( $unique_groups as $unique_group ) { 158 | $time = as_get_datetime_object( $i . ' hours' ); 159 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 160 | $action = new ActionScheduler_Action( 'my_hook', [ $i ], $schedule, $unique_group ); 161 | 162 | $created_actions[ $unique_group ][] = $store->save_action( $action ); 163 | } 164 | } 165 | 166 | $claim = $store->stake_claim( 10, null, array(), $unique_group_one ); 167 | $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); 168 | $this->assertCount( 3, $claim->get_actions() ); 169 | $this->assertEqualSets( array_slice( $created_actions[ $unique_group_one ], 3, 3 ), $claim->get_actions() ); 170 | 171 | $store->release_claim( $claim ); 172 | 173 | $claim = $store->stake_claim( 10, null, array(), $unique_group_two ); 174 | $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); 175 | $this->assertCount( 3, $claim->get_actions() ); 176 | $this->assertEqualSets( array_slice( $created_actions[ $unique_group_two ], 3, 3 ), $claim->get_actions() ); 177 | } 178 | 179 | public function test_claim_actions_by_hook_and_group() { 180 | $created_actions = $created_actions_by_hook = []; 181 | $store = new DB_Store(); 182 | 183 | $unique_hook_one = 'my_other_unique_hook_one'; 184 | $unique_hook_two = 'my_other_unique_hook_two'; 185 | $unique_hooks = array( 186 | $unique_hook_one, 187 | $unique_hook_two, 188 | ); 189 | 190 | $unique_group_one = 'my_other_other_unique_group_one'; 191 | $unique_group_two = 'my_other_unique_group_two'; 192 | $unique_groups = array( 193 | $unique_group_one, 194 | $unique_group_two, 195 | ); 196 | 197 | for ( $i = 3; $i > - 3; $i -- ) { 198 | foreach ( $unique_hooks as $unique_hook ) { 199 | foreach ( $unique_groups as $unique_group ) { 200 | $time = as_get_datetime_object( $i . ' hours' ); 201 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 202 | $action = new ActionScheduler_Action( $unique_hook, [ $i ], $schedule, $unique_group ); 203 | 204 | $action_id = $store->save_action( $action ); 205 | $created_actions[ $unique_group ][] = $action_id; 206 | $created_actions_by_hook[ $unique_hook ][ $unique_group ][] = $action_id; 207 | } 208 | } 209 | } 210 | 211 | /** Test Both Hooks with Each Group */ 212 | 213 | $claim = $store->stake_claim( 10, null, $unique_hooks, $unique_group_one ); 214 | $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); 215 | $this->assertCount( 6, $claim->get_actions() ); 216 | $this->assertEqualSets( array_slice( $created_actions[ $unique_group_one ], 6, 6 ), $claim->get_actions() ); 217 | 218 | $store->release_claim( $claim ); 219 | 220 | $claim = $store->stake_claim( 10, null, $unique_hooks, $unique_group_two ); 221 | $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); 222 | $this->assertCount( 6, $claim->get_actions() ); 223 | $this->assertEqualSets( array_slice( $created_actions[ $unique_group_two ], 6, 6 ), $claim->get_actions() ); 224 | 225 | $store->release_claim( $claim ); 226 | 227 | /** Test Just One Hook with Group One */ 228 | 229 | $claim = $store->stake_claim( 10, null, array( $unique_hook_one ), $unique_group_one ); 230 | $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); 231 | $this->assertCount( 3, $claim->get_actions() ); 232 | $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_one ][ $unique_group_one ], 3, 3 ), $claim->get_actions() ); 233 | 234 | $store->release_claim( $claim ); 235 | 236 | $claim = $store->stake_claim( 24, null, array( $unique_hook_two ), $unique_group_one ); 237 | $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); 238 | $this->assertCount( 3, $claim->get_actions() ); 239 | $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_two ][ $unique_group_one ], 3, 3 ), $claim->get_actions() ); 240 | 241 | $store->release_claim( $claim ); 242 | 243 | /** Test Just One Hook with Group Two */ 244 | 245 | $claim = $store->stake_claim( 10, null, array( $unique_hook_one ), $unique_group_two ); 246 | $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); 247 | $this->assertCount( 3, $claim->get_actions() ); 248 | $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_one ][ $unique_group_two ], 3, 3 ), $claim->get_actions() ); 249 | 250 | $store->release_claim( $claim ); 251 | 252 | $claim = $store->stake_claim( 24, null, array( $unique_hook_two ), $unique_group_two ); 253 | $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); 254 | $this->assertCount( 3, $claim->get_actions() ); 255 | $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_two ][ $unique_group_two ], 3, 3 ), $claim->get_actions() ); 256 | } 257 | 258 | public function test_duplicate_claim() { 259 | $created_actions = []; 260 | $store = new DB_Store(); 261 | for ( $i = 0; $i > - 3; $i -- ) { 262 | $time = as_get_datetime_object( $i . ' hours' ); 263 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 264 | $action = new ActionScheduler_Action( 'my_hook', [ $i ], $schedule, 'my_group' ); 265 | 266 | $created_actions[] = $store->save_action( $action ); 267 | } 268 | 269 | $claim1 = $store->stake_claim(); 270 | $claim2 = $store->stake_claim(); 271 | $this->assertCount( 3, $claim1->get_actions() ); 272 | $this->assertCount( 0, $claim2->get_actions() ); 273 | } 274 | 275 | public function test_release_claim() { 276 | $created_actions = []; 277 | $store = new DB_Store(); 278 | for ( $i = 0; $i > - 3; $i -- ) { 279 | $time = as_get_datetime_object( $i . ' hours' ); 280 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 281 | $action = new ActionScheduler_Action( 'my_hook', [ $i ], $schedule, 'my_group' ); 282 | 283 | $created_actions[] = $store->save_action( $action ); 284 | } 285 | 286 | $claim1 = $store->stake_claim(); 287 | 288 | $store->release_claim( $claim1 ); 289 | 290 | $claim2 = $store->stake_claim(); 291 | $this->assertCount( 3, $claim2->get_actions() ); 292 | } 293 | 294 | public function test_search() { 295 | $created_actions = []; 296 | $store = new DB_Store(); 297 | for ( $i = - 3; $i <= 3; $i ++ ) { 298 | $time = as_get_datetime_object( $i . ' hours' ); 299 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 300 | $action = new ActionScheduler_Action( 'my_hook', [ $i ], $schedule, 'my_group' ); 301 | 302 | $created_actions[] = $store->save_action( $action ); 303 | } 304 | 305 | $next_no_args = $store->find_action( 'my_hook' ); 306 | $this->assertEquals( $created_actions[ 0 ], $next_no_args ); 307 | 308 | $next_with_args = $store->find_action( 'my_hook', [ 'args' => [ 1 ] ] ); 309 | $this->assertEquals( $created_actions[ 4 ], $next_with_args ); 310 | 311 | $non_existent = $store->find_action( 'my_hook', [ 'args' => [ 17 ] ] ); 312 | $this->assertNull( $non_existent ); 313 | } 314 | 315 | public function test_search_by_group() { 316 | $store = new DB_Store(); 317 | $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) ); 318 | 319 | $abc = $store->save_action( new ActionScheduler_Action( 'my_hook', [ 1 ], $schedule, 'abc' ) ); 320 | $def = $store->save_action( new ActionScheduler_Action( 'my_hook', [ 1 ], $schedule, 'def' ) ); 321 | $ghi = $store->save_action( new ActionScheduler_Action( 'my_hook', [ 1 ], $schedule, 'ghi' ) ); 322 | 323 | $this->assertEquals( $abc, $store->find_action( 'my_hook', [ 'group' => 'abc' ] ) ); 324 | $this->assertEquals( $def, $store->find_action( 'my_hook', [ 'group' => 'def' ] ) ); 325 | $this->assertEquals( $ghi, $store->find_action( 'my_hook', [ 'group' => 'ghi' ] ) ); 326 | } 327 | 328 | public function test_get_run_date() { 329 | $time = as_get_datetime_object( '-10 minutes' ); 330 | $schedule = new ActionScheduler_IntervalSchedule( $time, HOUR_IN_SECONDS ); 331 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule ); 332 | $store = new DB_Store(); 333 | $action_id = $store->save_action( $action ); 334 | 335 | $this->assertEquals( $time->format( 'U' ), $store->get_date( $action_id )->format( 'U' ) ); 336 | 337 | $action = $store->fetch_action( $action_id ); 338 | $action->execute(); 339 | $now = as_get_datetime_object(); 340 | $store->mark_complete( $action_id ); 341 | 342 | $this->assertEquals( $now->format( 'U' ), $store->get_date( $action_id )->format( 'U' ) ); 343 | 344 | $next = $action->get_schedule()->next( $now ); 345 | $new_action_id = $store->save_action( $action, $next ); 346 | 347 | $this->assertEquals( (int) ( $now->format( 'U' ) ) + HOUR_IN_SECONDS, $store->get_date( $new_action_id )->format( 'U' ) ); 348 | } 349 | 350 | public function test_get_status() { 351 | $time = as_get_datetime_object('-10 minutes'); 352 | $schedule = new ActionScheduler_IntervalSchedule($time, HOUR_IN_SECONDS); 353 | $action = new ActionScheduler_Action('my_hook', array(), $schedule); 354 | $store = new DB_Store(); 355 | $action_id = $store->save_action($action); 356 | 357 | $this->assertEquals( ActionScheduler_Store::STATUS_PENDING, $store->get_status( $action_id ) ); 358 | 359 | $store->mark_complete( $action_id ); 360 | $this->assertEquals( ActionScheduler_Store::STATUS_COMPLETE, $store->get_status( $action_id ) ); 361 | 362 | $store->mark_failure( $action_id ); 363 | $this->assertEquals( ActionScheduler_Store::STATUS_FAILED, $store->get_status( $action_id ) ); 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /tests/phpunit/jobstore/Hybrid_Store_Test.php: -------------------------------------------------------------------------------- 1 | init(); 23 | } 24 | update_option( Hybrid_Store::DEMARKATION_OPTION, $this->demarkation_id ); 25 | $hybrid = new Hybrid_Store(); 26 | $hybrid->set_autoincrement( '', DB_Store_Table_Maker::ACTIONS_TABLE ); 27 | } 28 | 29 | public function tearDown() { 30 | parent::tearDown(); 31 | 32 | // reset the autoincrement index 33 | /** @var \wpdb $wpdb */ 34 | global $wpdb; 35 | $wpdb->query( "TRUNCATE TABLE {$wpdb->actionscheduler_actions}" ); 36 | delete_option( Hybrid_Store::DEMARKATION_OPTION ); 37 | } 38 | 39 | public function test_actions_are_migrated_on_find() { 40 | $source_store = new PostStore(); 41 | $destination_store = new DB_Store(); 42 | $source_logger = new CommentLogger(); 43 | $destination_logger = new DB_Logger(); 44 | 45 | $config = new Migration_Config(); 46 | $config->set_source_store( $source_store ); 47 | $config->set_source_logger( $source_logger ); 48 | $config->set_destination_store( $destination_store ); 49 | $config->set_destination_logger( $destination_logger ); 50 | 51 | $hybrid_store = new Hybrid_Store( $config ); 52 | 53 | $time = as_get_datetime_object( '10 minutes ago' ); 54 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 55 | $action = new ActionScheduler_Action( __FUNCTION__, [], $schedule ); 56 | $source_id = $source_store->save_action( $action ); 57 | 58 | $found = $hybrid_store->find_action( __FUNCTION__, [] ); 59 | 60 | $this->assertNotEquals( $source_id, $found ); 61 | $this->assertGreaterThanOrEqual( $this->demarkation_id, $found ); 62 | 63 | $found_in_source = $source_store->fetch_action( $source_id ); 64 | $this->assertInstanceOf( NullAction::class, $found_in_source ); 65 | } 66 | 67 | 68 | public function test_actions_are_migrated_on_query() { 69 | $source_store = new PostStore(); 70 | $destination_store = new DB_Store(); 71 | $source_logger = new CommentLogger(); 72 | $destination_logger = new DB_Logger(); 73 | 74 | $config = new Migration_Config(); 75 | $config->set_source_store( $source_store ); 76 | $config->set_source_logger( $source_logger ); 77 | $config->set_destination_store( $destination_store ); 78 | $config->set_destination_logger( $destination_logger ); 79 | 80 | $hybrid_store = new Hybrid_Store( $config ); 81 | 82 | $source_actions = []; 83 | $destination_actions = []; 84 | 85 | for ( $i = 0; $i < 10; $i++ ) { 86 | // create in instance in the source store 87 | $time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes' ); 88 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 89 | $action = new ActionScheduler_Action( __FUNCTION__, [], $schedule ); 90 | 91 | $source_actions[] = $source_store->save_action( $action ); 92 | 93 | // create an instance in the destination store 94 | $time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes' ); 95 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 96 | $action = new ActionScheduler_Action( __FUNCTION__, [], $schedule ); 97 | 98 | $destination_actions[] = $destination_store->save_action( $action ); 99 | } 100 | 101 | $found = $hybrid_store->query_actions([ 102 | 'hook' => __FUNCTION__, 103 | 'per_page' => 6, 104 | ] ); 105 | 106 | $this->assertCount( 6, $found ); 107 | foreach ( $found as $key => $action_id ) { 108 | $this->assertNotContains( $action_id, $source_actions ); 109 | $this->assertGreaterThanOrEqual( $this->demarkation_id, $action_id ); 110 | if ( $key % 2 == 0 ) { // it should have been in the source store 111 | $this->assertNotContains( $action_id, $destination_actions ); 112 | } else { // it should have already been in the destination store 113 | $this->assertContains( $action_id, $destination_actions ); 114 | } 115 | } 116 | 117 | // six of the original 10 should have migrated to the new store 118 | // even though only three were retrieve in the final query 119 | $found_in_source = $source_store->query_actions( [ 120 | 'hook' => __FUNCTION__, 121 | 'per_page' => 10, 122 | ] ); 123 | $this->assertCount( 4, $found_in_source ); 124 | } 125 | 126 | 127 | public function test_actions_are_migrated_on_claim() { 128 | $source_store = new PostStore(); 129 | $destination_store = new DB_Store(); 130 | $source_logger = new CommentLogger(); 131 | $destination_logger = new DB_Logger(); 132 | 133 | $config = new Migration_Config(); 134 | $config->set_source_store( $source_store ); 135 | $config->set_source_logger( $source_logger ); 136 | $config->set_destination_store( $destination_store ); 137 | $config->set_destination_logger( $destination_logger ); 138 | 139 | $hybrid_store = new Hybrid_Store( $config ); 140 | 141 | $source_actions = []; 142 | $destination_actions = []; 143 | 144 | for ( $i = 0; $i < 10; $i++ ) { 145 | // create in instance in the source store 146 | $time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes ago' ); 147 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 148 | $action = new ActionScheduler_Action( __FUNCTION__, [], $schedule ); 149 | 150 | $source_actions[] = $source_store->save_action( $action ); 151 | 152 | // create an instance in the destination store 153 | $time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes ago' ); 154 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 155 | $action = new ActionScheduler_Action( __FUNCTION__, [], $schedule ); 156 | 157 | $destination_actions[] = $destination_store->save_action( $action ); 158 | } 159 | 160 | $claim = $hybrid_store->stake_claim( 6 ); 161 | 162 | $claimed_actions = $claim->get_actions(); 163 | $this->assertCount( 6, $claimed_actions ); 164 | $this->assertCount( 3, array_intersect( $destination_actions, $claimed_actions ) ); 165 | 166 | 167 | // six of the original 10 should have migrated to the new store 168 | // even though only three were retrieve in the final claim 169 | $found_in_source = $source_store->query_actions( [ 170 | 'hook' => __FUNCTION__, 171 | 'per_page' => 10, 172 | ] ); 173 | $this->assertCount( 4, $found_in_source ); 174 | 175 | $this->assertEquals( 0, $source_store->get_claim_count() ); 176 | $this->assertEquals( 1, $destination_store->get_claim_count() ); 177 | $this->assertEquals( 1, $hybrid_store->get_claim_count() ); 178 | 179 | } 180 | 181 | public function test_fetch_respects_demarkation() { 182 | $source_store = new PostStore(); 183 | $destination_store = new DB_Store(); 184 | $source_logger = new CommentLogger(); 185 | $destination_logger = new DB_Logger(); 186 | 187 | $config = new Migration_Config(); 188 | $config->set_source_store( $source_store ); 189 | $config->set_source_logger( $source_logger ); 190 | $config->set_destination_store( $destination_store ); 191 | $config->set_destination_logger( $destination_logger ); 192 | 193 | $hybrid_store = new Hybrid_Store( $config ); 194 | 195 | $source_actions = []; 196 | $destination_actions = []; 197 | 198 | for ( $i = 0; $i < 2; $i++ ) { 199 | // create in instance in the source store 200 | $time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes ago' ); 201 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 202 | $action = new ActionScheduler_Action( __FUNCTION__, [], $schedule ); 203 | 204 | $source_actions[] = $source_store->save_action( $action ); 205 | 206 | // create an instance in the destination store 207 | $time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes ago' ); 208 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 209 | $action = new ActionScheduler_Action( __FUNCTION__, [], $schedule ); 210 | 211 | $destination_actions[] = $destination_store->save_action( $action ); 212 | } 213 | 214 | foreach ( $source_actions as $action_id ) { 215 | $action = $hybrid_store->fetch_action( $action_id ); 216 | $this->assertInstanceOf( ActionScheduler_Action::class, $action ); 217 | $this->assertNotInstanceOf( NullAction::class, $action ); 218 | } 219 | 220 | foreach ( $destination_actions as $action_id ) { 221 | $action = $hybrid_store->fetch_action( $action_id ); 222 | $this->assertInstanceOf( ActionScheduler_Action::class, $action ); 223 | $this->assertNotInstanceOf( NullAction::class, $action ); 224 | } 225 | } 226 | 227 | public function test_mark_complete_respects_demarkation() { 228 | $source_store = new PostStore(); 229 | $destination_store = new DB_Store(); 230 | $source_logger = new CommentLogger(); 231 | $destination_logger = new DB_Logger(); 232 | 233 | $config = new Migration_Config(); 234 | $config->set_source_store( $source_store ); 235 | $config->set_source_logger( $source_logger ); 236 | $config->set_destination_store( $destination_store ); 237 | $config->set_destination_logger( $destination_logger ); 238 | 239 | $hybrid_store = new Hybrid_Store( $config ); 240 | 241 | $source_actions = []; 242 | $destination_actions = []; 243 | 244 | for ( $i = 0; $i < 2; $i++ ) { 245 | // create in instance in the source store 246 | $time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes ago' ); 247 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 248 | $action = new ActionScheduler_Action( __FUNCTION__, [], $schedule ); 249 | 250 | $source_actions[] = $source_store->save_action( $action ); 251 | 252 | // create an instance in the destination store 253 | $time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes ago' ); 254 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 255 | $action = new ActionScheduler_Action( __FUNCTION__, [], $schedule ); 256 | 257 | $destination_actions[] = $destination_store->save_action( $action ); 258 | } 259 | 260 | foreach ( $source_actions as $action_id ) { 261 | $hybrid_store->mark_complete( $action_id ); 262 | $action = $hybrid_store->fetch_action( $action_id ); 263 | $this->assertInstanceOf( ActionScheduler_FinishedAction::class, $action ); 264 | } 265 | 266 | foreach ( $destination_actions as $action_id ) { 267 | $hybrid_store->mark_complete( $action_id ); 268 | $action = $hybrid_store->fetch_action( $action_id ); 269 | $this->assertInstanceOf( ActionScheduler_FinishedAction::class, $action ); 270 | } 271 | } 272 | } -------------------------------------------------------------------------------- /tests/phpunit/logging/DB_Logger_Test.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf( 'ActionScheduler_Logger', $logger ); 17 | $this->assertInstanceOf( DB_Logger::class, $logger ); 18 | } 19 | 20 | public function test_add_log_entry() { 21 | $action_id = as_schedule_single_action( time(), __METHOD__ ); 22 | $logger = ActionScheduler::logger(); 23 | $message = 'Logging that something happened'; 24 | $log_id = $logger->log( $action_id, $message ); 25 | $entry = $logger->get_entry( $log_id ); 26 | 27 | $this->assertEquals( $action_id, $entry->get_action_id() ); 28 | $this->assertEquals( $message, $entry->get_message() ); 29 | } 30 | 31 | public function test_null_log_entry() { 32 | $logger = ActionScheduler::logger(); 33 | $entry = $logger->get_entry( 1 ); 34 | $this->assertEquals( '', $entry->get_action_id() ); 35 | $this->assertEquals( '', $entry->get_message() ); 36 | } 37 | 38 | public function test_storage_logs() { 39 | $action_id = as_schedule_single_action( time(), __METHOD__ ); 40 | $logger = ActionScheduler::logger(); 41 | $logs = $logger->get_logs( $action_id ); 42 | $expected = new ActionScheduler_LogEntry( $action_id, 'action created' ); 43 | $this->assertCount( 1, $logs ); 44 | $this->assertEquals( $expected->get_action_id(), $logs[0]->get_action_id() ); 45 | $this->assertEquals( $expected->get_message(), $logs[0]->get_message() ); 46 | } 47 | 48 | public function test_execution_logs() { 49 | $action_id = as_schedule_single_action( time(), __METHOD__ ); 50 | $logger = ActionScheduler::logger(); 51 | $started = new ActionScheduler_LogEntry( $action_id, 'action started' ); 52 | $finished = new ActionScheduler_LogEntry( $action_id, 'action complete' ); 53 | 54 | $runner = new ActionScheduler_QueueRunner(); 55 | $runner->run(); 56 | 57 | // Expect 3 logs with the correct action ID. 58 | $logs = $logger->get_logs( $action_id ); 59 | $this->assertCount( 3, $logs ); 60 | foreach ( $logs as $log ) { 61 | $this->assertEquals( $action_id, $log->get_action_id() ); 62 | } 63 | 64 | // Expect created, then started, then completed. 65 | $this->assertEquals( 'action created', $logs[0]->get_message() ); 66 | $this->assertEquals( $started->get_message(), $logs[1]->get_message() ); 67 | $this->assertEquals( $finished->get_message(), $logs[2]->get_message() ); 68 | } 69 | 70 | public function test_failed_execution_logs() { 71 | $hook = __METHOD__; 72 | add_action( $hook, array( $this, '_a_hook_callback_that_throws_an_exception' ) ); 73 | $action_id = as_schedule_single_action( time(), $hook ); 74 | $logger = ActionScheduler::logger(); 75 | $started = new ActionScheduler_LogEntry( $action_id, 'action started' ); 76 | $finished = new ActionScheduler_LogEntry( $action_id, 'action complete' ); 77 | $failed = new ActionScheduler_LogEntry( $action_id, 'action failed: Execution failed' ); 78 | 79 | $runner = new ActionScheduler_QueueRunner(); 80 | $runner->run(); 81 | 82 | // Expect 3 logs with the correct action ID. 83 | $logs = $logger->get_logs( $action_id ); 84 | $this->assertCount( 3, $logs ); 85 | foreach ( $logs as $log ) { 86 | $this->assertEquals( $action_id, $log->get_action_id() ); 87 | $this->assertNotEquals( $finished->get_message(), $log->get_message() ); 88 | } 89 | 90 | // Expect created, then started, then failed. 91 | $this->assertEquals( 'action created', $logs[0]->get_message() ); 92 | $this->assertEquals( $started->get_message(), $logs[1]->get_message() ); 93 | $this->assertEquals( $failed->get_message(), $logs[2]->get_message() ); 94 | } 95 | 96 | public function test_fatal_error_log() { 97 | $action_id = as_schedule_single_action( time(), __METHOD__ ); 98 | $logger = ActionScheduler::logger(); 99 | do_action( 'action_scheduler_unexpected_shutdown', $action_id, array( 100 | 'type' => E_ERROR, 101 | 'message' => 'Test error', 102 | 'file' => __FILE__, 103 | 'line' => __LINE__, 104 | )); 105 | 106 | $logs = $logger->get_logs( $action_id ); 107 | $found_log = FALSE; 108 | foreach ( $logs as $l ) { 109 | if ( strpos( $l->get_message(), 'unexpected shutdown' ) === 0 ) { 110 | $found_log = TRUE; 111 | } 112 | } 113 | $this->assertTrue( $found_log, 'Unexpected shutdown log not found' ); 114 | } 115 | 116 | public function test_canceled_action_log() { 117 | $action_id = as_schedule_single_action( time(), __METHOD__ ); 118 | as_unschedule_action( __METHOD__ ); 119 | $logger = ActionScheduler::logger(); 120 | $logs = $logger->get_logs( $action_id ); 121 | $expected = new ActionScheduler_LogEntry( $action_id, 'action canceled' ); 122 | $this->assertEquals( $expected->get_message(), end( $logs )->get_message() ); 123 | } 124 | 125 | public function test_deleted_action_cleanup() { 126 | $time = as_get_datetime_object('-10 minutes'); 127 | $schedule = new \ActionScheduler_SimpleSchedule($time); 128 | $action = new \ActionScheduler_Action('my_hook', array(), $schedule); 129 | $store = new DB_Store(); 130 | $action_id = $store->save_action($action); 131 | 132 | $logger = new DB_Logger(); 133 | $logs = $logger->get_logs( $action_id ); 134 | $this->assertNotEmpty( $logs ); 135 | 136 | $store->delete_action( $action_id ); 137 | $logs = $logger->get_logs( $action_id ); 138 | $this->assertEmpty( $logs ); 139 | } 140 | 141 | public function _a_hook_callback_that_throws_an_exception() { 142 | throw new \RuntimeException('Execution failed'); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/phpunit/migration/Action_Migrator_Test.php: -------------------------------------------------------------------------------- 1 | init(); 18 | } 19 | } 20 | 21 | public function test_migrate_from_wpPost_to_db() { 22 | $source = new ActionScheduler_wpPostStore(); 23 | $destination = new DB_Store(); 24 | $migrator = new Action_Migrator( $source, $destination, $this->get_log_migrator() ); 25 | 26 | $time = as_get_datetime_object(); 27 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 28 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule, 'my_group' ); 29 | $action_id = $source->save_action( $action ); 30 | 31 | $new_id = $migrator->migrate( $action_id ); 32 | 33 | // ensure we get the same record out of the new store as we stored in the old 34 | $retrieved = $destination->fetch_action( $new_id ); 35 | $this->assertEquals( $action->get_hook(), $retrieved->get_hook() ); 36 | $this->assertEqualSets( $action->get_args(), $retrieved->get_args() ); 37 | $this->assertEquals( $action->get_schedule()->next()->format( 'U' ), $retrieved->get_schedule()->next()->format( 'U' ) ); 38 | $this->assertEquals( $action->get_group(), $retrieved->get_group() ); 39 | $this->assertEquals( \ActionScheduler_Store::STATUS_PENDING, $destination->get_status( $new_id ) ); 40 | 41 | 42 | // ensure that the record in the old store does not exist 43 | $old_action = $source->fetch_action( $action_id ); 44 | $this->assertInstanceOf( 'ActionScheduler_NullAction', $old_action ); 45 | } 46 | 47 | public function test_does_not_migrate_missing_action_from_wpPost_to_db() { 48 | $source = new ActionScheduler_wpPostStore(); 49 | $destination = new DB_Store(); 50 | $migrator = new Action_Migrator( $source, $destination, $this->get_log_migrator() ); 51 | 52 | $action_id = rand( 100, 100000 ); 53 | 54 | $new_id = $migrator->migrate( $action_id ); 55 | $this->assertEquals( 0, $new_id ); 56 | 57 | // ensure we get the same record out of the new store as we stored in the old 58 | $retrieved = $destination->fetch_action( $new_id ); 59 | $this->assertInstanceOf( 'ActionScheduler_NullAction', $retrieved ); 60 | } 61 | 62 | public function test_migrate_completed_action_from_wpPost_to_db() { 63 | $source = new ActionScheduler_wpPostStore(); 64 | $destination = new DB_Store(); 65 | $migrator = new Action_Migrator( $source, $destination, $this->get_log_migrator() ); 66 | 67 | $time = as_get_datetime_object(); 68 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 69 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule, 'my_group' ); 70 | $action_id = $source->save_action( $action ); 71 | $source->mark_complete( $action_id ); 72 | 73 | $new_id = $migrator->migrate( $action_id ); 74 | 75 | // ensure we get the same record out of the new store as we stored in the old 76 | $retrieved = $destination->fetch_action( $new_id ); 77 | $this->assertEquals( $action->get_hook(), $retrieved->get_hook() ); 78 | $this->assertEqualSets( $action->get_args(), $retrieved->get_args() ); 79 | $this->assertEquals( $action->get_schedule()->next()->format( 'U' ), $retrieved->get_schedule()->next()->format( 'U' ) ); 80 | $this->assertEquals( $action->get_group(), $retrieved->get_group() ); 81 | $this->assertTrue( $retrieved->is_finished() ); 82 | $this->assertEquals( \ActionScheduler_Store::STATUS_COMPLETE, $destination->get_status( $new_id ) ); 83 | 84 | // ensure that the record in the old store does not exist 85 | $old_action = $source->fetch_action( $action_id ); 86 | $this->assertInstanceOf( 'ActionScheduler_NullAction', $old_action ); 87 | } 88 | 89 | public function test_migrate_failed_action_from_wpPost_to_db() { 90 | $source = new ActionScheduler_wpPostStore(); 91 | $destination = new DB_Store(); 92 | $migrator = new Action_Migrator( $source, $destination, $this->get_log_migrator() ); 93 | 94 | $time = as_get_datetime_object(); 95 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 96 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule, 'my_group' ); 97 | $action_id = $source->save_action( $action ); 98 | $source->mark_failure( $action_id ); 99 | 100 | $new_id = $migrator->migrate( $action_id ); 101 | 102 | // ensure we get the same record out of the new store as we stored in the old 103 | $retrieved = $destination->fetch_action( $new_id ); 104 | $this->assertEquals( $action->get_hook(), $retrieved->get_hook() ); 105 | $this->assertEqualSets( $action->get_args(), $retrieved->get_args() ); 106 | $this->assertEquals( $action->get_schedule()->next()->format( 'U' ), $retrieved->get_schedule()->next()->format( 'U' ) ); 107 | $this->assertEquals( $action->get_group(), $retrieved->get_group() ); 108 | $this->assertTrue( $retrieved->is_finished() ); 109 | $this->assertEquals( \ActionScheduler_Store::STATUS_FAILED, $destination->get_status( $new_id ) ); 110 | 111 | // ensure that the record in the old store does not exist 112 | $old_action = $source->fetch_action( $action_id ); 113 | $this->assertInstanceOf( 'ActionScheduler_NullAction', $old_action ); 114 | } 115 | 116 | public function test_does_not_migrate_canceled_action_from_wpPost_to_db() { 117 | $source = new ActionScheduler_wpPostStore(); 118 | $destination = new DB_Store(); 119 | $migrator = new Action_Migrator( $source, $destination, $this->get_log_migrator() ); 120 | 121 | $time = as_get_datetime_object(); 122 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 123 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule, 'my_group' ); 124 | $action_id = $source->save_action( $action ); 125 | $source->cancel_action( $action_id ); 126 | 127 | $new_id = $migrator->migrate( $action_id ); 128 | 129 | $this->assertEquals( 0, $new_id ); 130 | 131 | // ensure that the record in the old store does not exist 132 | $old_action = $source->fetch_action( $action_id ); 133 | $this->assertInstanceOf( 'ActionScheduler_NullAction', $old_action ); 134 | } 135 | 136 | private function get_log_migrator() { 137 | return new Log_Migrator( \ActionScheduler::logger(), new DB_Logger() ); 138 | } 139 | } -------------------------------------------------------------------------------- /tests/phpunit/migration/Batch_Fetcher_Test.php: -------------------------------------------------------------------------------- 1 | init(); 20 | } 21 | } 22 | 23 | public function test_nothing_to_migrate() { 24 | $store = new PostStore(); 25 | $batch_fetcher = new Batch_Fetcher( $store ); 26 | 27 | $actions = $batch_fetcher->fetch(); 28 | $this->assertEmpty( $actions ); 29 | } 30 | 31 | public function test_get_due_before_future() { 32 | $store = new PostStore(); 33 | $due = []; 34 | $future = []; 35 | 36 | for ( $i = 0; $i < 5; $i ++ ) { 37 | $time = as_get_datetime_object( $i + 1 . ' minutes' ); 38 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 39 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule ); 40 | $future[] = $store->save_action( $action ); 41 | 42 | $time = as_get_datetime_object( $i + 1 . ' minutes ago' ); 43 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 44 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule ); 45 | $due[] = $store->save_action( $action ); 46 | } 47 | 48 | $batch_fetcher = new Batch_Fetcher( $store ); 49 | 50 | $actions = $batch_fetcher->fetch(); 51 | 52 | $this->assertEqualSets( $due, $actions ); 53 | } 54 | 55 | 56 | public function test_get_future_before_complete() { 57 | $store = new PostStore(); 58 | $future = []; 59 | $complete = []; 60 | 61 | for ( $i = 0; $i < 5; $i ++ ) { 62 | $time = as_get_datetime_object( $i + 1 . ' minutes' ); 63 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 64 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule ); 65 | $future[] = $store->save_action( $action ); 66 | 67 | $time = as_get_datetime_object( $i + 1 . ' minutes ago' ); 68 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 69 | $action = new ActionScheduler_FinishedAction( 'my_hook', [], $schedule ); 70 | $complete[] = $store->save_action( $action ); 71 | } 72 | 73 | $batch_fetcher = new Batch_Fetcher( $store ); 74 | 75 | $actions = $batch_fetcher->fetch(); 76 | 77 | $this->assertEqualSets( $future, $actions ); 78 | } 79 | } -------------------------------------------------------------------------------- /tests/phpunit/migration/Log_Migrator_Test.php: -------------------------------------------------------------------------------- 1 | init(); 19 | } 20 | } 21 | 22 | public function test_migrate_from_wpComment_to_db() { 23 | $source = new ActionScheduler_wpCommentLogger(); 24 | $destination = new DB_Logger(); 25 | $migrator = new Log_Migrator( $source, $destination ); 26 | $source_action_id = rand( 10, 10000 ); 27 | $destination_action_id = rand( 10, 10000 ); 28 | 29 | $logs = []; 30 | for ( $i = 0 ; $i < 3 ; $i++ ) { 31 | for ( $j = 0 ; $j < 5 ; $j++ ) { 32 | $logs[ $i ][ $j ] = md5(rand()); 33 | if ( $i == 1 ) { 34 | $source->log( $source_action_id, $logs[ $i ][ $j ] ); 35 | } 36 | } 37 | } 38 | 39 | $migrator->migrate( $source_action_id, $destination_action_id ); 40 | 41 | $migrated = $destination->get_logs( $destination_action_id ); 42 | $this->assertEqualSets( $logs[ 1 ], array_map( function( $log ) { return $log->get_message(); }, $migrated ) ); 43 | 44 | // no API for deleting logs, so we leave them for manual cleanup later 45 | $this->assertCount( 5, $source->get_logs( $source_action_id ) ); 46 | } 47 | } -------------------------------------------------------------------------------- /tests/phpunit/migration/Migration_Config_Test.php: -------------------------------------------------------------------------------- 1 | expectException( \RuntimeException::class ); 12 | $config->get_source_store(); 13 | } 14 | 15 | public function test_source_logger_required() { 16 | $config = new Migration_Config(); 17 | $this->expectException( \RuntimeException::class ); 18 | $config->get_source_logger(); 19 | } 20 | 21 | public function test_destination_store_required() { 22 | $config = new Migration_Config(); 23 | $this->expectException( \RuntimeException::class ); 24 | $config->get_destination_store(); 25 | } 26 | 27 | public function test_destination_logger_required() { 28 | $config = new Migration_Config(); 29 | $this->expectException( \RuntimeException::class ); 30 | $config->get_destination_logger(); 31 | } 32 | } -------------------------------------------------------------------------------- /tests/phpunit/migration/Migration_Runner_Test.php: -------------------------------------------------------------------------------- 1 | init(); 22 | } 23 | } 24 | 25 | public function test_migrate_batches() { 26 | $source_store = new PostStore(); 27 | $destination_store = new DB_Store(); 28 | $source_logger = new CommentLogger(); 29 | $destination_logger = new DB_Logger(); 30 | 31 | $config = new Migration_Config(); 32 | $config->set_source_store( $source_store ); 33 | $config->set_source_logger( $source_logger ); 34 | $config->set_destination_store( $destination_store ); 35 | $config->set_destination_logger( $destination_logger ); 36 | 37 | $runner = new Migration_Runner( $config ); 38 | 39 | $due = []; 40 | $future = []; 41 | $complete = []; 42 | 43 | for ( $i = 0; $i < 5; $i ++ ) { 44 | $time = as_get_datetime_object( $i + 1 . ' minutes' ); 45 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 46 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule ); 47 | $future[] = $source_store->save_action( $action ); 48 | 49 | $time = as_get_datetime_object( $i + 1 . ' minutes ago' ); 50 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 51 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule ); 52 | $due[] = $source_store->save_action( $action ); 53 | 54 | $time = as_get_datetime_object( $i + 1 . ' minutes ago' ); 55 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 56 | $action = new ActionScheduler_FinishedAction( 'my_hook', [], $schedule ); 57 | $complete[] = $source_store->save_action( $action ); 58 | } 59 | 60 | $created = $source_store->query_actions( [ 'per_page' => 0 ] ); 61 | $this->assertCount( 15, $created ); 62 | 63 | $runner->run( 10 ); 64 | 65 | // due actions should migrate in the first batch 66 | $migrated = $destination_store->query_actions( [ 'per_page' => 0 ] ); 67 | $this->assertCount( 5, $migrated ); 68 | 69 | $remaining = $source_store->query_actions( [ 'per_page' => 0 ] ); 70 | $this->assertCount( 10, $remaining ); 71 | 72 | 73 | $runner->run( 10 ); 74 | 75 | // pending actions should migrate in the second batch 76 | $migrated = $destination_store->query_actions( [ 'per_page' => 0 ] ); 77 | $this->assertCount( 10, $migrated ); 78 | 79 | $remaining = $source_store->query_actions( [ 'per_page' => 0 ] ); 80 | $this->assertCount( 5, $remaining ); 81 | 82 | 83 | $runner->run( 10 ); 84 | 85 | // completed actions should migrate in the third batch 86 | $migrated = $destination_store->query_actions( [ 'per_page' => 0 ] ); 87 | $this->assertCount( 15, $migrated ); 88 | 89 | $remaining = $source_store->query_actions( [ 'per_page' => 0 ] ); 90 | $this->assertCount( 0, $remaining ); 91 | 92 | } 93 | 94 | } -------------------------------------------------------------------------------- /tests/phpunit/migration/Migration_Scheduler_Test.php: -------------------------------------------------------------------------------- 1 | init(); 18 | } 19 | } 20 | 21 | public function test_migration_is_complete() { 22 | $scheduler = new Migration_Scheduler(); 23 | update_option( Migration_Scheduler::STATUS_FLAG, Migration_Scheduler::STATUS_COMPLETE ); 24 | $this->assertTrue( $scheduler->is_migration_complete() ); 25 | } 26 | 27 | public function test_migration_is_not_complete() { 28 | $scheduler = new Migration_Scheduler(); 29 | $this->assertFalse( $scheduler->is_migration_complete() ); 30 | update_option( Migration_Scheduler::STATUS_FLAG, 'something_random' ); 31 | $this->assertFalse( $scheduler->is_migration_complete() ); 32 | } 33 | 34 | public function test_migration_is_scheduled() { 35 | $scheduler = new Migration_Scheduler(); 36 | $scheduler->schedule_migration(); 37 | $this->assertTrue( $scheduler->is_migration_scheduled() ); 38 | } 39 | 40 | public function test_migration_is_not_scheduled() { 41 | $scheduler = new Migration_Scheduler(); 42 | $this->assertFalse( $scheduler->is_migration_scheduled() ); 43 | } 44 | 45 | public function test_scheduler_runs_migration() { 46 | $source_store = new PostStore(); 47 | $destination_store = new DB_Store(); 48 | 49 | $return_5 = function () { 50 | return 5; 51 | }; 52 | add_filter( 'action_scheduler/custom_tables/migration_batch_size', $return_5 ); 53 | 54 | for ( $i = 0; $i < 10; $i ++ ) { 55 | $time = as_get_datetime_object( $i + 1 . ' minutes' ); 56 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 57 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule ); 58 | $future[] = $source_store->save_action( $action ); 59 | 60 | $time = as_get_datetime_object( $i + 1 . ' minutes ago' ); 61 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 62 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule ); 63 | $due[] = $source_store->save_action( $action ); 64 | } 65 | 66 | $this->assertCount( 20, $source_store->query_actions( [ 'per_page' => 0 ] ) ); 67 | 68 | $scheduler = new Migration_Scheduler(); 69 | $scheduler->schedule_migration(); 70 | 71 | $queue_runner = new \ActionScheduler_QueueRunner( $destination_store ); 72 | $queue_runner->run(); 73 | 74 | // 5 actions should have moved from the source store when the queue runner triggered the migration action 75 | $this->assertCount( 15, $source_store->query_actions( [ 'per_page' => 0 ] ) ); 76 | 77 | remove_filter( 'action_scheduler/custom_tables/migration_batch_size', $return_5 ); 78 | } 79 | 80 | public function test_scheduler_marks_itself_complete() { 81 | $source_store = new PostStore(); 82 | $destination_store = new DB_Store(); 83 | 84 | for ( $i = 0; $i < 5; $i ++ ) { 85 | $time = as_get_datetime_object( $i + 1 . ' minutes ago' ); 86 | $schedule = new ActionScheduler_SimpleSchedule( $time ); 87 | $action = new ActionScheduler_Action( 'my_hook', [], $schedule ); 88 | $due[] = $source_store->save_action( $action ); 89 | } 90 | 91 | $this->assertCount( 5, $source_store->query_actions( [ 'per_page' => 0 ] ) ); 92 | 93 | $scheduler = new Migration_Scheduler(); 94 | $scheduler->schedule_migration(); 95 | 96 | $queue_runner = new \ActionScheduler_QueueRunner( $destination_store ); 97 | $queue_runner->run(); 98 | 99 | // All actions should have moved from the source store when the queue runner triggered the migration action 100 | $this->assertCount( 0, $source_store->query_actions( [ 'per_page' => 0 ] ) ); 101 | 102 | // schedule another so we can get it to run immediately 103 | $scheduler->unschedule_migration(); 104 | $scheduler->schedule_migration(); 105 | 106 | // run again so it knows that there's nothing left to process 107 | $queue_runner->run(); 108 | 109 | $scheduler->unhook(); 110 | 111 | // ensure the flag is set marking migration as complete 112 | $this->assertTrue( $scheduler->is_migration_complete() ); 113 | 114 | // ensure that another instance has not been scheduled 115 | $this->assertFalse( $scheduler->is_migration_scheduled() ); 116 | 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/travis/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # WordPress test setup script for Travis CI 4 | # 5 | # Author: Benjamin J. Balter ( ben@balter.com | ben.balter.com ) 6 | # License: GPL3 7 | 8 | export WP_CORE_DIR=/tmp/wordpress 9 | export WP_TESTS_DIR=/tmp/wordpress-tests/tests/phpunit 10 | 11 | wget -c https://phar.phpunit.de/phpunit-5.7.phar 12 | chmod +x phpunit-5.7.phar 13 | mv phpunit-5.7.phar `which phpunit` 14 | 15 | plugin_slug=$(basename $(pwd)) 16 | plugin_dir=$WP_CORE_DIR/wp-content/plugins/$plugin_slug 17 | 18 | # Init database 19 | mysql -e 'CREATE DATABASE wordpress_test;' -uroot 20 | 21 | # Grab specified version of WordPress from github 22 | wget -nv -O /tmp/wordpress.tar.gz https://github.com/WordPress/WordPress/tarball/$WP_VERSION 23 | mkdir -p $WP_CORE_DIR 24 | tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR 25 | 26 | # Grab testing framework 27 | svn co --quiet https://develop.svn.wordpress.org/tags/$WP_VERSION/ /tmp/wordpress-tests 28 | 29 | # Put various components in proper folders 30 | cp tests/travis/wp-tests-config.php $WP_TESTS_DIR/wp-tests-config.php 31 | 32 | # Grab the specified branch of Action Scheduler from github 33 | wget -nv -O /tmp/action-scheduler.tgz "https://github.com/Prospress/action-scheduler/tarball/$AS_VERSION" 34 | mkdir -p "$WP_CORE_DIR/wp-content/plugins/action-scheduler" 35 | tar --strip-components=1 -zxmf /tmp/action-scheduler.tgz -C "$WP_CORE_DIR/wp-content/plugins/action-scheduler" 36 | 37 | cd .. 38 | mv $plugin_slug $plugin_dir 39 | 40 | cd $plugin_dir 41 | -------------------------------------------------------------------------------- /tests/travis/wp-tests-config.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer\Autoload; 14 | 15 | /** 16 | * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. 17 | * 18 | * $loader = new \Composer\Autoload\ClassLoader(); 19 | * 20 | * // register classes with namespaces 21 | * $loader->add('Symfony\Component', __DIR__.'/component'); 22 | * $loader->add('Symfony', __DIR__.'/framework'); 23 | * 24 | * // activate the autoloader 25 | * $loader->register(); 26 | * 27 | * // to enable searching the include path (eg. for PEAR packages) 28 | * $loader->setUseIncludePath(true); 29 | * 30 | * In this example, if you try to use a class in the Symfony\Component 31 | * namespace or one of its children (Symfony\Component\Console for instance), 32 | * the autoloader will first look for the class under the component/ 33 | * directory, and it will then fallback to the framework/ directory if not 34 | * found before giving up. 35 | * 36 | * This class is loosely based on the Symfony UniversalClassLoader. 37 | * 38 | * @author Fabien Potencier 39 | * @author Jordi Boggiano 40 | * @see http://www.php-fig.org/psr/psr-0/ 41 | * @see http://www.php-fig.org/psr/psr-4/ 42 | */ 43 | class ClassLoader 44 | { 45 | // PSR-4 46 | private $prefixLengthsPsr4 = array(); 47 | private $prefixDirsPsr4 = array(); 48 | private $fallbackDirsPsr4 = array(); 49 | 50 | // PSR-0 51 | private $prefixesPsr0 = array(); 52 | private $fallbackDirsPsr0 = array(); 53 | 54 | private $useIncludePath = false; 55 | private $classMap = array(); 56 | private $classMapAuthoritative = false; 57 | private $missingClasses = array(); 58 | private $apcuPrefix; 59 | 60 | public function getPrefixes() 61 | { 62 | if (!empty($this->prefixesPsr0)) { 63 | return call_user_func_array('array_merge', $this->prefixesPsr0); 64 | } 65 | 66 | return array(); 67 | } 68 | 69 | public function getPrefixesPsr4() 70 | { 71 | return $this->prefixDirsPsr4; 72 | } 73 | 74 | public function getFallbackDirs() 75 | { 76 | return $this->fallbackDirsPsr0; 77 | } 78 | 79 | public function getFallbackDirsPsr4() 80 | { 81 | return $this->fallbackDirsPsr4; 82 | } 83 | 84 | public function getClassMap() 85 | { 86 | return $this->classMap; 87 | } 88 | 89 | /** 90 | * @param array $classMap Class to filename map 91 | */ 92 | public function addClassMap(array $classMap) 93 | { 94 | if ($this->classMap) { 95 | $this->classMap = array_merge($this->classMap, $classMap); 96 | } else { 97 | $this->classMap = $classMap; 98 | } 99 | } 100 | 101 | /** 102 | * Registers a set of PSR-0 directories for a given prefix, either 103 | * appending or prepending to the ones previously set for this prefix. 104 | * 105 | * @param string $prefix The prefix 106 | * @param array|string $paths The PSR-0 root directories 107 | * @param bool $prepend Whether to prepend the directories 108 | */ 109 | public function add($prefix, $paths, $prepend = false) 110 | { 111 | if (!$prefix) { 112 | if ($prepend) { 113 | $this->fallbackDirsPsr0 = array_merge( 114 | (array) $paths, 115 | $this->fallbackDirsPsr0 116 | ); 117 | } else { 118 | $this->fallbackDirsPsr0 = array_merge( 119 | $this->fallbackDirsPsr0, 120 | (array) $paths 121 | ); 122 | } 123 | 124 | return; 125 | } 126 | 127 | $first = $prefix[0]; 128 | if (!isset($this->prefixesPsr0[$first][$prefix])) { 129 | $this->prefixesPsr0[$first][$prefix] = (array) $paths; 130 | 131 | return; 132 | } 133 | if ($prepend) { 134 | $this->prefixesPsr0[$first][$prefix] = array_merge( 135 | (array) $paths, 136 | $this->prefixesPsr0[$first][$prefix] 137 | ); 138 | } else { 139 | $this->prefixesPsr0[$first][$prefix] = array_merge( 140 | $this->prefixesPsr0[$first][$prefix], 141 | (array) $paths 142 | ); 143 | } 144 | } 145 | 146 | /** 147 | * Registers a set of PSR-4 directories for a given namespace, either 148 | * appending or prepending to the ones previously set for this namespace. 149 | * 150 | * @param string $prefix The prefix/namespace, with trailing '\\' 151 | * @param array|string $paths The PSR-4 base directories 152 | * @param bool $prepend Whether to prepend the directories 153 | * 154 | * @throws \InvalidArgumentException 155 | */ 156 | public function addPsr4($prefix, $paths, $prepend = false) 157 | { 158 | if (!$prefix) { 159 | // Register directories for the root namespace. 160 | if ($prepend) { 161 | $this->fallbackDirsPsr4 = array_merge( 162 | (array) $paths, 163 | $this->fallbackDirsPsr4 164 | ); 165 | } else { 166 | $this->fallbackDirsPsr4 = array_merge( 167 | $this->fallbackDirsPsr4, 168 | (array) $paths 169 | ); 170 | } 171 | } elseif (!isset($this->prefixDirsPsr4[$prefix])) { 172 | // Register directories for a new namespace. 173 | $length = strlen($prefix); 174 | if ('\\' !== $prefix[$length - 1]) { 175 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 176 | } 177 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 178 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 179 | } elseif ($prepend) { 180 | // Prepend directories for an already registered namespace. 181 | $this->prefixDirsPsr4[$prefix] = array_merge( 182 | (array) $paths, 183 | $this->prefixDirsPsr4[$prefix] 184 | ); 185 | } else { 186 | // Append directories for an already registered namespace. 187 | $this->prefixDirsPsr4[$prefix] = array_merge( 188 | $this->prefixDirsPsr4[$prefix], 189 | (array) $paths 190 | ); 191 | } 192 | } 193 | 194 | /** 195 | * Registers a set of PSR-0 directories for a given prefix, 196 | * replacing any others previously set for this prefix. 197 | * 198 | * @param string $prefix The prefix 199 | * @param array|string $paths The PSR-0 base directories 200 | */ 201 | public function set($prefix, $paths) 202 | { 203 | if (!$prefix) { 204 | $this->fallbackDirsPsr0 = (array) $paths; 205 | } else { 206 | $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; 207 | } 208 | } 209 | 210 | /** 211 | * Registers a set of PSR-4 directories for a given namespace, 212 | * replacing any others previously set for this namespace. 213 | * 214 | * @param string $prefix The prefix/namespace, with trailing '\\' 215 | * @param array|string $paths The PSR-4 base directories 216 | * 217 | * @throws \InvalidArgumentException 218 | */ 219 | public function setPsr4($prefix, $paths) 220 | { 221 | if (!$prefix) { 222 | $this->fallbackDirsPsr4 = (array) $paths; 223 | } else { 224 | $length = strlen($prefix); 225 | if ('\\' !== $prefix[$length - 1]) { 226 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 227 | } 228 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 229 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 230 | } 231 | } 232 | 233 | /** 234 | * Turns on searching the include path for class files. 235 | * 236 | * @param bool $useIncludePath 237 | */ 238 | public function setUseIncludePath($useIncludePath) 239 | { 240 | $this->useIncludePath = $useIncludePath; 241 | } 242 | 243 | /** 244 | * Can be used to check if the autoloader uses the include path to check 245 | * for classes. 246 | * 247 | * @return bool 248 | */ 249 | public function getUseIncludePath() 250 | { 251 | return $this->useIncludePath; 252 | } 253 | 254 | /** 255 | * Turns off searching the prefix and fallback directories for classes 256 | * that have not been registered with the class map. 257 | * 258 | * @param bool $classMapAuthoritative 259 | */ 260 | public function setClassMapAuthoritative($classMapAuthoritative) 261 | { 262 | $this->classMapAuthoritative = $classMapAuthoritative; 263 | } 264 | 265 | /** 266 | * Should class lookup fail if not found in the current class map? 267 | * 268 | * @return bool 269 | */ 270 | public function isClassMapAuthoritative() 271 | { 272 | return $this->classMapAuthoritative; 273 | } 274 | 275 | /** 276 | * APCu prefix to use to cache found/not-found classes, if the extension is enabled. 277 | * 278 | * @param string|null $apcuPrefix 279 | */ 280 | public function setApcuPrefix($apcuPrefix) 281 | { 282 | $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; 283 | } 284 | 285 | /** 286 | * The APCu prefix in use, or null if APCu caching is not enabled. 287 | * 288 | * @return string|null 289 | */ 290 | public function getApcuPrefix() 291 | { 292 | return $this->apcuPrefix; 293 | } 294 | 295 | /** 296 | * Registers this instance as an autoloader. 297 | * 298 | * @param bool $prepend Whether to prepend the autoloader or not 299 | */ 300 | public function register($prepend = false) 301 | { 302 | spl_autoload_register(array($this, 'loadClass'), true, $prepend); 303 | } 304 | 305 | /** 306 | * Unregisters this instance as an autoloader. 307 | */ 308 | public function unregister() 309 | { 310 | spl_autoload_unregister(array($this, 'loadClass')); 311 | } 312 | 313 | /** 314 | * Loads the given class or interface. 315 | * 316 | * @param string $class The name of the class 317 | * @return bool|null True if loaded, null otherwise 318 | */ 319 | public function loadClass($class) 320 | { 321 | if ($file = $this->findFile($class)) { 322 | includeFile($file); 323 | 324 | return true; 325 | } 326 | } 327 | 328 | /** 329 | * Finds the path to the file where the class is defined. 330 | * 331 | * @param string $class The name of the class 332 | * 333 | * @return string|false The path if found, false otherwise 334 | */ 335 | public function findFile($class) 336 | { 337 | // class map lookup 338 | if (isset($this->classMap[$class])) { 339 | return $this->classMap[$class]; 340 | } 341 | if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { 342 | return false; 343 | } 344 | if (null !== $this->apcuPrefix) { 345 | $file = apcu_fetch($this->apcuPrefix.$class, $hit); 346 | if ($hit) { 347 | return $file; 348 | } 349 | } 350 | 351 | $file = $this->findFileWithExtension($class, '.php'); 352 | 353 | // Search for Hack files if we are running on HHVM 354 | if (false === $file && defined('HHVM_VERSION')) { 355 | $file = $this->findFileWithExtension($class, '.hh'); 356 | } 357 | 358 | if (null !== $this->apcuPrefix) { 359 | apcu_add($this->apcuPrefix.$class, $file); 360 | } 361 | 362 | if (false === $file) { 363 | // Remember that this class does not exist. 364 | $this->missingClasses[$class] = true; 365 | } 366 | 367 | return $file; 368 | } 369 | 370 | private function findFileWithExtension($class, $ext) 371 | { 372 | // PSR-4 lookup 373 | $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; 374 | 375 | $first = $class[0]; 376 | if (isset($this->prefixLengthsPsr4[$first])) { 377 | $subPath = $class; 378 | while (false !== $lastPos = strrpos($subPath, '\\')) { 379 | $subPath = substr($subPath, 0, $lastPos); 380 | $search = $subPath.'\\'; 381 | if (isset($this->prefixDirsPsr4[$search])) { 382 | foreach ($this->prefixDirsPsr4[$search] as $dir) { 383 | $length = $this->prefixLengthsPsr4[$first][$search]; 384 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { 385 | return $file; 386 | } 387 | } 388 | } 389 | } 390 | } 391 | 392 | // PSR-4 fallback dirs 393 | foreach ($this->fallbackDirsPsr4 as $dir) { 394 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { 395 | return $file; 396 | } 397 | } 398 | 399 | // PSR-0 lookup 400 | if (false !== $pos = strrpos($class, '\\')) { 401 | // namespaced class name 402 | $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) 403 | . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); 404 | } else { 405 | // PEAR-like class name 406 | $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; 407 | } 408 | 409 | if (isset($this->prefixesPsr0[$first])) { 410 | foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { 411 | if (0 === strpos($class, $prefix)) { 412 | foreach ($dirs as $dir) { 413 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 414 | return $file; 415 | } 416 | } 417 | } 418 | } 419 | } 420 | 421 | // PSR-0 fallback dirs 422 | foreach ($this->fallbackDirsPsr0 as $dir) { 423 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 424 | return $file; 425 | } 426 | } 427 | 428 | // PSR-0 include paths. 429 | if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { 430 | return $file; 431 | } 432 | 433 | return false; 434 | } 435 | } 436 | 437 | /** 438 | * Scope isolated include. 439 | * 440 | * Prevents access to $this/self from included files. 441 | */ 442 | function includeFile($file) 443 | { 444 | include $file; 445 | } 446 | -------------------------------------------------------------------------------- /vendor/composer/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) Nils Adermann, Jordi Boggiano 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /vendor/composer/autoload_classmap.php: -------------------------------------------------------------------------------- 1 | array($baseDir . '/src'), 10 | ); 11 | -------------------------------------------------------------------------------- /vendor/composer/autoload_real.php: -------------------------------------------------------------------------------- 1 | = 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); 27 | if ($useStaticLoader) { 28 | require_once __DIR__ . '/autoload_static.php'; 29 | 30 | call_user_func(\Composer\Autoload\ComposerStaticInitdf0ffd033d1385935fc4656f332be27b::getInitializer($loader)); 31 | } else { 32 | $map = require __DIR__ . '/autoload_namespaces.php'; 33 | foreach ($map as $namespace => $path) { 34 | $loader->set($namespace, $path); 35 | } 36 | 37 | $map = require __DIR__ . '/autoload_psr4.php'; 38 | foreach ($map as $namespace => $path) { 39 | $loader->setPsr4($namespace, $path); 40 | } 41 | 42 | $classMap = require __DIR__ . '/autoload_classmap.php'; 43 | if ($classMap) { 44 | $loader->addClassMap($classMap); 45 | } 46 | } 47 | 48 | $loader->register(true); 49 | 50 | return $loader; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /vendor/composer/autoload_static.php: -------------------------------------------------------------------------------- 1 | 11 | array ( 12 | 'Action_Scheduler\\Custom_Tables\\' => 31, 13 | ), 14 | ); 15 | 16 | public static $prefixDirsPsr4 = array ( 17 | 'Action_Scheduler\\Custom_Tables\\' => 18 | array ( 19 | 0 => __DIR__ . '/../..' . '/src', 20 | ), 21 | ); 22 | 23 | public static function getInitializer(ClassLoader $loader) 24 | { 25 | return \Closure::bind(function () use ($loader) { 26 | $loader->prefixLengthsPsr4 = ComposerStaticInitdf0ffd033d1385935fc4656f332be27b::$prefixLengthsPsr4; 27 | $loader->prefixDirsPsr4 = ComposerStaticInitdf0ffd033d1385935fc4656f332be27b::$prefixDirsPsr4; 28 | 29 | }, null, ClassLoader::class); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /vendor/composer/installed.json: -------------------------------------------------------------------------------- 1 | [] 2 | --------------------------------------------------------------------------------