Initial Commit

This commit is contained in:
David Stone
2024-11-30 18:24:12 -07:00
commit e8f7955c1c
5432 changed files with 1397750 additions and 0 deletions

View File

@ -0,0 +1,109 @@
<?php
namespace Action_Scheduler\Migration;
/**
* Class ActionMigrator
*
* @package Action_Scheduler\Migration
*
* @since 3.0.0
*
* @codeCoverageIgnore
*/
class ActionMigrator {
/** var ActionScheduler_Store */
private $source;
/** var ActionScheduler_Store */
private $destination;
/** var LogMigrator */
private $log_migrator;
/**
* ActionMigrator constructor.
*
* @param ActionScheduler_Store $source_store Source store object.
* @param ActionScheduler_Store $destination_store Destination store object.
* @param LogMigrator $log_migrator Log migrator object.
*/
public function __construct( \ActionScheduler_Store $source_store, \ActionScheduler_Store $destination_store, LogMigrator $log_migrator ) {
$this->source = $source_store;
$this->destination = $destination_store;
$this->log_migrator = $log_migrator;
}
/**
* Migrate an action.
*
* @param int $source_action_id Action ID.
*
* @return int 0|new action ID
*/
public function migrate( $source_action_id ) {
try {
$action = $this->source->fetch_action( $source_action_id );
$status = $this->source->get_status( $source_action_id );
} catch ( \Exception $e ) {
$action = null;
$status = '';
}
if ( is_null( $action ) || empty( $status ) || ! $action->get_schedule()->get_date() ) {
// null action or empty status means the fetch operation failed or the action didn't exist
// null schedule means it's missing vital data
// delete it and move on
try {
$this->source->delete_action( $source_action_id );
} catch ( \Exception $e ) {
// nothing to do, it didn't exist in the first place
}
do_action( 'action_scheduler/no_action_to_migrate', $source_action_id, $this->source, $this->destination );
return 0;
}
try {
// Make sure the last attempt date is set correctly for completed and failed actions
$last_attempt_date = ( $status !== \ActionScheduler_Store::STATUS_PENDING ) ? $this->source->get_date( $source_action_id ) : null;
$destination_action_id = $this->destination->save_action( $action, null, $last_attempt_date );
} catch ( \Exception $e ) {
do_action( 'action_scheduler/migrate_action_failed', $source_action_id, $this->source, $this->destination );
return 0; // could not save the action in the new store
}
try {
switch ( $status ) {
case \ActionScheduler_Store::STATUS_FAILED :
$this->destination->mark_failure( $destination_action_id );
break;
case \ActionScheduler_Store::STATUS_CANCELED :
$this->destination->cancel_action( $destination_action_id );
break;
}
$this->log_migrator->migrate( $source_action_id, $destination_action_id );
$this->source->delete_action( $source_action_id );
$test_action = $this->source->fetch_action( $source_action_id );
if ( ! is_a( $test_action, 'ActionScheduler_NullAction' ) ) {
throw new \RuntimeException( sprintf( __( 'Unable to remove source migrated action %s', 'action-scheduler' ), $source_action_id ) );
}
do_action( 'action_scheduler/migrated_action', $source_action_id, $destination_action_id, $this->source, $this->destination );
return $destination_action_id;
} catch ( \Exception $e ) {
// could not delete from the old store
$this->source->mark_migrated( $source_action_id );
do_action( 'action_scheduler/migrate_action_incomplete', $source_action_id, $destination_action_id, $this->source, $this->destination );
do_action( 'action_scheduler/migrated_action', $source_action_id, $destination_action_id, $this->source, $this->destination );
return $destination_action_id;
}
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* Class ActionScheduler_DBStoreMigrator
*
* A class for direct saving of actions to the table data store during migration.
*
* @since 3.0.0
*/
class ActionScheduler_DBStoreMigrator extends ActionScheduler_DBStore {
/**
* Save an action with optional last attempt date.
*
* Normally, saving an action sets its attempted date to 0000-00-00 00:00:00 because when an action is first saved,
* it can't have been attempted yet, but migrated completed actions will have an attempted date, so we need to save
* that when first saving the action.
*
* @param ActionScheduler_Action $action
* @param \DateTime $scheduled_date Optional date of the first instance to store.
* @param \DateTime $last_attempt_date Optional date the action was last attempted.
*
* @return string The action ID
* @throws \RuntimeException When the action is not saved.
*/
public function save_action( ActionScheduler_Action $action, \DateTime $scheduled_date = null, \DateTime $last_attempt_date = null ){
try {
/** @var \wpdb $wpdb */
global $wpdb;
$action_id = parent::save_action( $action, $scheduled_date );
if ( null !== $last_attempt_date ) {
$data = [
'last_attempt_gmt' => $this->get_scheduled_date_string( $action, $last_attempt_date ),
'last_attempt_local' => $this->get_scheduled_date_string_local( $action, $last_attempt_date ),
];
$wpdb->update( $wpdb->actionscheduler_actions, $data, array( 'action_id' => $action_id ), array( '%s', '%s' ), array( '%d' ) );
}
return $action_id;
} catch ( \Exception $e ) {
throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'action-scheduler' ), $e->getMessage() ), 0 );
}
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace Action_Scheduler\Migration;
use ActionScheduler_Store as Store;
/**
* Class BatchFetcher
*
* @package Action_Scheduler\Migration
*
* @since 3.0.0
*
* @codeCoverageIgnore
*/
class BatchFetcher {
/** var ActionScheduler_Store */
private $store;
/**
* BatchFetcher constructor.
*
* @param ActionScheduler_Store $source_store Source store object.
*/
public function __construct( Store $source_store ) {
$this->store = $source_store;
}
/**
* Retrieve a list of actions.
*
* @param int $count The number of actions to retrieve
*
* @return int[] A list of action IDs
*/
public function fetch( $count = 10 ) {
foreach ( $this->get_query_strategies( $count ) as $query ) {
$action_ids = $this->store->query_actions( $query );
if ( ! empty( $action_ids ) ) {
return $action_ids;
}
}
return [];
}
/**
* Generate a list of prioritized of action search parameters.
*
* @param int $count Number of actions to find.
*
* @return array
*/
private function get_query_strategies( $count ) {
$now = as_get_datetime_object();
$args = [
'date' => $now,
'per_page' => $count,
'offset' => 0,
'orderby' => 'date',
'order' => 'ASC',
];
$priorities = [
Store::STATUS_PENDING,
Store::STATUS_FAILED,
Store::STATUS_CANCELED,
Store::STATUS_COMPLETE,
Store::STATUS_RUNNING,
'', // any other unanticipated status
];
foreach ( $priorities as $status ) {
yield wp_parse_args( [
'status' => $status,
'date_compare' => '<=',
], $args );
yield wp_parse_args( [
'status' => $status,
'date_compare' => '>=',
], $args );
}
}
}

View File

@ -0,0 +1,168 @@
<?php
namespace Action_Scheduler\Migration;
use Action_Scheduler\WP_CLI\ProgressBar;
use ActionScheduler_Logger as Logger;
use ActionScheduler_Store as Store;
/**
* Class Config
*
* @package Action_Scheduler\Migration
*
* @since 3.0.0
*
* A config builder for the ActionScheduler\Migration\Runner class
*/
class Config {
/** @var ActionScheduler_Store */
private $source_store;
/** @var ActionScheduler_Logger */
private $source_logger;
/** @var ActionScheduler_Store */
private $destination_store;
/** @var ActionScheduler_Logger */
private $destination_logger;
/** @var Progress bar */
private $progress_bar;
/** @var bool */
private $dry_run = false;
/**
* Config constructor.
*/
public function __construct() {
}
/**
* Get the configured source store.
*
* @return ActionScheduler_Store
*/
public function get_source_store() {
if ( empty( $this->source_store ) ) {
throw new \RuntimeException( __( 'Source store must be configured before running a migration', 'action-scheduler' ) );
}
return $this->source_store;
}
/**
* Set the configured source store.
*
* @param ActionScheduler_Store $store Source store object.
*/
public function set_source_store( Store $store ) {
$this->source_store = $store;
}
/**
* Get the configured source loger.
*
* @return ActionScheduler_Logger
*/
public function get_source_logger() {
if ( empty( $this->source_logger ) ) {
throw new \RuntimeException( __( 'Source logger must be configured before running a migration', 'action-scheduler' ) );
}
return $this->source_logger;
}
/**
* Set the configured source logger.
*
* @param ActionScheduler_Logger $logger
*/
public function set_source_logger( Logger $logger ) {
$this->source_logger = $logger;
}
/**
* Get the configured destination store.
*
* @return ActionScheduler_Store
*/
public function get_destination_store() {
if ( empty( $this->destination_store ) ) {
throw new \RuntimeException( __( 'Destination store must be configured before running a migration', 'action-scheduler' ) );
}
return $this->destination_store;
}
/**
* Set the configured destination store.
*
* @param ActionScheduler_Store $store
*/
public function set_destination_store( Store $store ) {
$this->destination_store = $store;
}
/**
* Get the configured destination logger.
*
* @return ActionScheduler_Logger
*/
public function get_destination_logger() {
if ( empty( $this->destination_logger ) ) {
throw new \RuntimeException( __( 'Destination logger must be configured before running a migration', 'action-scheduler' ) );
}
return $this->destination_logger;
}
/**
* Set the configured destination logger.
*
* @param ActionScheduler_Logger $logger
*/
public function set_destination_logger( Logger $logger ) {
$this->destination_logger = $logger;
}
/**
* Get flag indicating whether it's a dry run.
*
* @return bool
*/
public function get_dry_run() {
return $this->dry_run;
}
/**
* Set flag indicating whether it's a dry run.
*
* @param bool $dry_run
*/
public function set_dry_run( $dry_run ) {
$this->dry_run = (bool) $dry_run;
}
/**
* Get progress bar object.
*
* @return ActionScheduler\WPCLI\ProgressBar
*/
public function get_progress_bar() {
return $this->progress_bar;
}
/**
* Set progress bar object.
*
* @param ActionScheduler\WPCLI\ProgressBar $progress_bar
*/
public function set_progress_bar( ProgressBar $progress_bar ) {
$this->progress_bar = $progress_bar;
}
}

View File

@ -0,0 +1,226 @@
<?php
namespace Action_Scheduler\Migration;
use ActionScheduler_DataController;
use ActionScheduler_LoggerSchema;
use ActionScheduler_StoreSchema;
use Action_Scheduler\WP_CLI\ProgressBar;
/**
* Class Controller
*
* The main plugin/initialization class for migration to custom tables.
*
* @package Action_Scheduler\Migration
*
* @since 3.0.0
*
* @codeCoverageIgnore
*/
class Controller {
private static $instance;
/** @var Action_Scheduler\Migration\Scheduler */
private $migration_scheduler;
/** @var string */
private $store_classname;
/** @var string */
private $logger_classname;
/** @var bool */
private $migrate_custom_store;
/**
* Controller constructor.
*
* @param Scheduler $migration_scheduler Migration scheduler object.
*/
protected function __construct( Scheduler $migration_scheduler ) {
$this->migration_scheduler = $migration_scheduler;
$this->store_classname = '';
}
/**
* Set the action store class name.
*
* @param string $class Classname of the store class.
*
* @return string
*/
public function get_store_class( $class ) {
if ( \ActionScheduler_DataController::is_migration_complete() ) {
return \ActionScheduler_DataController::DATASTORE_CLASS;
} elseif ( \ActionScheduler_Store::DEFAULT_CLASS !== $class ) {
$this->store_classname = $class;
return $class;
} else {
return 'ActionScheduler_HybridStore';
}
}
/**
* Set the action logger class name.
*
* @param string $class Classname of the logger class.
*
* @return string
*/
public function get_logger_class( $class ) {
\ActionScheduler_Store::instance();
if ( $this->has_custom_datastore() ) {
$this->logger_classname = $class;
return $class;
} else {
return \ActionScheduler_DataController::LOGGER_CLASS;
}
}
/**
* Get flag indicating whether a custom datastore is in use.
*
* @return bool
*/
public function has_custom_datastore() {
return (bool) $this->store_classname;
}
/**
* Set up the background migration process.
*
* @return void
*/
public function schedule_migration() {
$logging_tables = new ActionScheduler_LoggerSchema();
$store_tables = new ActionScheduler_StoreSchema();
/*
* In some unusual cases, the expected tables may not have been created. In such cases
* we do not schedule a migration as doing so will lead to fatal error conditions.
*
* In such cases the user will likely visit the Tools > Scheduled Actions screen to
* investigate, and will see appropriate messaging (this step also triggers an attempt
* to rebuild any missing tables).
*
* @see https://github.com/woocommerce/action-scheduler/issues/653
*/
if (
ActionScheduler_DataController::is_migration_complete()
|| $this->migration_scheduler->is_migration_scheduled()
|| ! $store_tables->tables_exist()
|| ! $logging_tables->tables_exist()
) {
return;
}
$this->migration_scheduler->schedule_migration();
}
/**
* Get the default migration config object
*
* @return ActionScheduler\Migration\Config
*/
public function get_migration_config_object() {
static $config = null;
if ( ! $config ) {
$source_store = $this->store_classname ? new $this->store_classname() : new \ActionScheduler_wpPostStore();
$source_logger = $this->logger_classname ? new $this->logger_classname() : new \ActionScheduler_wpCommentLogger();
$config = new Config();
$config->set_source_store( $source_store );
$config->set_source_logger( $source_logger );
$config->set_destination_store( new \ActionScheduler_DBStoreMigrator() );
$config->set_destination_logger( new \ActionScheduler_DBLogger() );
if ( defined( 'WP_CLI' ) && WP_CLI ) {
$config->set_progress_bar( new ProgressBar( '', 0 ) );
}
}
return apply_filters( 'action_scheduler/migration_config', $config );
}
/**
* Hook dashboard migration notice.
*/
public function hook_admin_notices() {
if ( ! $this->allow_migration() || \ActionScheduler_DataController::is_migration_complete() ) {
return;
}
add_action( 'admin_notices', array( $this, 'display_migration_notice' ), 10, 0 );
}
/**
* Show a dashboard notice that migration is in progress.
*/
public function display_migration_notice() {
printf( '<div class="notice notice-warning"><p>%s</p></div>', esc_html__( 'Action Scheduler migration in progress. The list of scheduled actions may be incomplete.', 'action-scheduler' ) );
}
/**
* Add store classes. Hook migration.
*/
private function hook() {
add_filter( 'action_scheduler_store_class', array( $this, 'get_store_class' ), 100, 1 );
add_filter( 'action_scheduler_logger_class', array( $this, 'get_logger_class' ), 100, 1 );
add_action( 'init', array( $this, 'maybe_hook_migration' ) );
add_action( 'wp_loaded', array( $this, 'schedule_migration' ) );
// Action Scheduler may be displayed as a Tools screen or WooCommerce > Status administration screen
add_action( 'load-tools_page_action-scheduler', array( $this, 'hook_admin_notices' ), 10, 0 );
add_action( 'load-woocommerce_page_wc-status', array( $this, 'hook_admin_notices' ), 10, 0 );
}
/**
* Possibly hook the migration scheduler action.
*
* @author Jeremy Pry
*/
public function maybe_hook_migration() {
if ( ! $this->allow_migration() || \ActionScheduler_DataController::is_migration_complete() ) {
return;
}
$this->migration_scheduler->hook();
}
/**
* Allow datastores to enable migration to AS tables.
*/
public function allow_migration() {
if ( ! \ActionScheduler_DataController::dependencies_met() ) {
return false;
}
if ( null === $this->migrate_custom_store ) {
$this->migrate_custom_store = apply_filters( 'action_scheduler_migrate_data_store', false );
}
return ( ! $this->has_custom_datastore() ) || $this->migrate_custom_store;
}
/**
* Proceed with the migration if the dependencies have been met.
*/
public static function init() {
if ( \ActionScheduler_DataController::dependencies_met() ) {
self::instance()->hook();
}
}
/**
* Singleton factory.
*/
public static function instance() {
if ( ! isset( self::$instance ) ) {
self::$instance = new static( new Scheduler() );
}
return self::$instance;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Action_Scheduler\Migration;
/**
* Class DryRun_ActionMigrator
*
* @package Action_Scheduler\Migration
*
* @since 3.0.0
*
* @codeCoverageIgnore
*/
class DryRun_ActionMigrator extends ActionMigrator {
/**
* Simulate migrating an action.
*
* @param int $source_action_id Action ID.
*
* @return int
*/
public function migrate( $source_action_id ) {
do_action( 'action_scheduler/migrate_action_dry_run', $source_action_id );
return 0;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Action_Scheduler\Migration;
/**
* Class DryRun_LogMigrator
*
* @package Action_Scheduler\Migration
*
* @codeCoverageIgnore
*/
class DryRun_LogMigrator extends LogMigrator {
/**
* Simulate migrating an action log.
*
* @param int $source_action_id Source logger object.
* @param int $destination_action_id Destination logger object.
*/
public function migrate( $source_action_id, $destination_action_id ) {
// no-op
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Action_Scheduler\Migration;
use ActionScheduler_Logger;
/**
* Class LogMigrator
*
* @package Action_Scheduler\Migration
*
* @since 3.0.0
*
* @codeCoverageIgnore
*/
class LogMigrator {
/** @var ActionScheduler_Logger */
private $source;
/** @var ActionScheduler_Logger */
private $destination;
/**
* ActionMigrator constructor.
*
* @param ActionScheduler_Logger $source_logger Source logger object.
* @param ActionScheduler_Logger $destination_Logger Destination logger object.
*/
public function __construct( ActionScheduler_Logger $source_logger, ActionScheduler_Logger $destination_Logger ) {
$this->source = $source_logger;
$this->destination = $destination_Logger;
}
/**
* Migrate an action log.
*
* @param int $source_action_id Source logger object.
* @param int $destination_action_id Destination logger object.
*/
public function migrate( $source_action_id, $destination_action_id ) {
$logs = $this->source->get_logs( $source_action_id );
foreach ( $logs as $log ) {
if ( $log->get_action_id() == $source_action_id ) {
$this->destination->log( $destination_action_id, $log->get_message(), $log->get_date() );
}
}
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace Action_Scheduler\Migration;
/**
* Class Runner
*
* @package Action_Scheduler\Migration
*
* @since 3.0.0
*
* @codeCoverageIgnore
*/
class Runner {
/** @var ActionScheduler_Store */
private $source_store;
/** @var ActionScheduler_Store */
private $destination_store;
/** @var ActionScheduler_Logger */
private $source_logger;
/** @var ActionScheduler_Logger */
private $destination_logger;
/** @var BatchFetcher */
private $batch_fetcher;
/** @var ActionMigrator */
private $action_migrator;
/** @var LogMigrator */
private $log_migrator;
/** @var ProgressBar */
private $progress_bar;
/**
* Runner constructor.
*
* @param Config $config Migration configuration object.
*/
public function __construct( Config $config ) {
$this->source_store = $config->get_source_store();
$this->destination_store = $config->get_destination_store();
$this->source_logger = $config->get_source_logger();
$this->destination_logger = $config->get_destination_logger();
$this->batch_fetcher = new BatchFetcher( $this->source_store );
if ( $config->get_dry_run() ) {
$this->log_migrator = new DryRun_LogMigrator( $this->source_logger, $this->destination_logger );
$this->action_migrator = new DryRun_ActionMigrator( $this->source_store, $this->destination_store, $this->log_migrator );
} else {
$this->log_migrator = new LogMigrator( $this->source_logger, $this->destination_logger );
$this->action_migrator = new ActionMigrator( $this->source_store, $this->destination_store, $this->log_migrator );
}
if ( defined( 'WP_CLI' ) && WP_CLI ) {
$this->progress_bar = $config->get_progress_bar();
}
}
/**
* Run migration batch.
*
* @param int $batch_size Optional batch size. Default 10.
*
* @return int Size of batch processed.
*/
public function run( $batch_size = 10 ) {
$batch = $this->batch_fetcher->fetch( $batch_size );
$batch_size = count( $batch );
if ( ! $batch_size ) {
return 0;
}
if ( $this->progress_bar ) {
/* translators: %d: amount of actions */
$this->progress_bar->set_message( sprintf( _n( 'Migrating %d action', 'Migrating %d actions', $batch_size, 'action-scheduler' ), $batch_size ) );
$this->progress_bar->set_count( $batch_size );
}
$this->migrate_actions( $batch );
return $batch_size;
}
/**
* Migration a batch of actions.
*
* @param array $action_ids List of action IDs to migrate.
*/
public function migrate_actions( array $action_ids ) {
do_action( 'action_scheduler/migration_batch_starting', $action_ids );
\ActionScheduler::logger()->unhook_stored_action();
$this->destination_logger->unhook_stored_action();
foreach ( $action_ids as $source_action_id ) {
$destination_action_id = $this->action_migrator->migrate( $source_action_id );
if ( $destination_action_id ) {
$this->destination_logger->log( $destination_action_id, sprintf(
/* translators: 1: source action ID 2: source store class 3: destination action ID 4: destination store class */
__( 'Migrated action with ID %1$d in %2$s to ID %3$d in %4$s', 'action-scheduler' ),
$source_action_id,
get_class( $this->source_store ),
$destination_action_id,
get_class( $this->destination_store )
) );
}
if ( $this->progress_bar ) {
$this->progress_bar->tick();
}
}
if ( $this->progress_bar ) {
$this->progress_bar->finish();
}
\ActionScheduler::logger()->hook_stored_action();
do_action( 'action_scheduler/migration_batch_complete', $action_ids );
}
/**
* Initialize destination store and logger.
*/
public function init_destination() {
$this->destination_store->init();
$this->destination_logger->init();
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace Action_Scheduler\Migration;
/**
* Class Scheduler
*
* @package Action_Scheduler\WP_CLI
*
* @since 3.0.0
*
* @codeCoverageIgnore
*/
class Scheduler {
/** Migration action hook. */
const HOOK = 'action_scheduler/migration_hook';
/** Migration action group. */
const GROUP = 'action-scheduler-migration';
/**
* Set up the callback for the scheduled job.
*/
public function hook() {
add_action( self::HOOK, array( $this, 'run_migration' ), 10, 0 );
}
/**
* Remove the callback for the scheduled job.
*/
public function unhook() {
remove_action( self::HOOK, array( $this, 'run_migration' ), 10 );
}
/**
* The migration callback.
*/
public function run_migration() {
$migration_runner = $this->get_migration_runner();
$count = $migration_runner->run( $this->get_batch_size() );
if ( $count === 0 ) {
$this->mark_complete();
} else {
$this->schedule_migration( time() + $this->get_schedule_interval() );
}
}
/**
* Mark the migration complete.
*/
public function mark_complete() {
$this->unschedule_migration();
\ActionScheduler_DataController::mark_migration_complete();
do_action( 'action_scheduler/migration_complete' );
}
/**
* Get a flag indicating whether the migration is scheduled.
*
* @return bool Whether there is a pending action in the store to handle the migration
*/
public function is_migration_scheduled() {
$next = as_next_scheduled_action( self::HOOK );
return ! empty( $next );
}
/**
* Schedule the migration.
*
* @param int $when Optional timestamp to run the next migration batch. Defaults to now.
*
* @return string The action ID
*/
public function schedule_migration( $when = 0 ) {
$next = as_next_scheduled_action( self::HOOK );
if ( ! empty( $next ) ) {
return $next;
}
if ( empty( $when ) ) {
$when = time() + MINUTE_IN_SECONDS;
}
return as_schedule_single_action( $when, self::HOOK, array(), self::GROUP );
}
/**
* Remove the scheduled migration action.
*/
public function unschedule_migration() {
as_unschedule_action( self::HOOK, null, self::GROUP );
}
/**
* Get migration batch schedule interval.
*
* @return int Seconds between migration runs. Defaults to 0 seconds to allow chaining migration via Async Runners.
*/
private function get_schedule_interval() {
return (int) apply_filters( 'action_scheduler/migration_interval', 0 );
}
/**
* Get migration batch size.
*
* @return int Number of actions to migrate in each batch. Defaults to 250.
*/
private function get_batch_size() {
return (int) apply_filters( 'action_scheduler/migration_batch_size', 250 );
}
/**
* Get migration runner object.
*
* @return Runner
*/
private function get_migration_runner() {
$config = Controller::instance()->get_migration_config_object();
return new Runner( $config );
}
}