├── .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 [](https://travis-ci.org/Prospress/action-scheduler-custom-tables) [](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( '', __( '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 |
--------------------------------------------------------------------------------