Infrastructure for the sync process

- Update settings UI
- Start sync via scheduled actions when sync is enabled
- Auto-switch authoritative table on sync finished if so configured
- Disable auto-switch if sync is disabled
- Show initial and current count of orders pending sync in settings UI
This commit is contained in:
Nestor Soriano 2022-03-08 16:54:45 +01:00
parent aa8b7a4a6b
commit 0114d3b5d6
No known key found for this signature in database
GPG Key ID: 08110F3518C12CAD
3 changed files with 331 additions and 22 deletions

View File

@ -7,6 +7,7 @@
*/ */
use Automattic\Jetpack\Constants; use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Utilities\ArrayUtil;
if ( ! defined( 'ABSPATH' ) ) { if ( ! defined( 'ABSPATH' ) ) {
exit; exit;
@ -273,6 +274,12 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
} }
break; break;
case 'info':
echo '<tr><th scope="row" class="titledesc"/><td style="' . esc_html( $value['css'] ) . '">';
echo wp_kses_post( wpautop( wptexturize( $value['text'] ) ) );
echo '</td></tr>';
break;
// Section Ends. // Section Ends.
case 'sectionend': case 'sectionend':
if ( ! empty( $value['id'] ) ) { if ( ! empty( $value['id'] ) ) {
@ -417,6 +424,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
// Radio inputs. // Radio inputs.
case 'radio': case 'radio':
$option_value = $value['value']; $option_value = $value['value'];
$disabled_values = ArrayUtil::get_value_or_default($value, 'disabled', array());
?> ?>
<tr valign="top"> <tr valign="top">
@ -435,6 +443,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
name="<?php echo esc_attr( $value['id'] ); ?>" name="<?php echo esc_attr( $value['id'] ); ?>"
value="<?php echo esc_attr( $key ); ?>" value="<?php echo esc_attr( $key ); ?>"
type="radio" type="radio"
<?php if(in_array($key, $disabled_values)) { echo 'disabled'; } ?>
style="<?php echo esc_attr( $value['css'] ); ?>" style="<?php echo esc_attr( $value['css'] ); ?>"
class="<?php echo esc_attr( $value['class'] ); ?>" class="<?php echo esc_attr( $value['class'] ); ?>"
<?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?> <?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?>

View File

@ -92,6 +92,38 @@ class CustomOrdersTableController {
999, 999,
2 2
); );
add_filter(
'updated_option',
function( $option, $old_value, $value ) {
$this->process_updated_option( $option, $old_value, $value );
},
999,
3
);
add_filter(
'pre_update_option',
function( $value, $option, $old_value ) {
return $this->process_pre_update_option( $option, $old_value, $value );
},
999,
3
);
add_filter(
DataSynchronizer::PENDING_SYNCHRONIZATION_FINISHED_ACTION,
function() {
$this->process_sync_finished();
}
);
add_action(
'woocommerce_update_options_advanced_custom_data_stores',
function() {
$this->process_options_updated();
}
);
} }
/** /**
@ -213,6 +245,7 @@ class CustomOrdersTableController {
} }
$this->data_synchronizer->create_database_tables(); $this->data_synchronizer->create_database_tables();
update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' );
} }
/** /**
@ -258,40 +291,182 @@ class CustomOrdersTableController {
return $settings; return $settings;
} }
$title_item = array(
'title' => __( 'Custom orders tables', 'woocommerce' ),
'type' => 'title',
'desc' => sprintf(
/* translators: %1$s = <strong> tag, %2$s = </strong> tag. */
__( '%1$sWARNING:%2$s This feature is currently under development and may cause database instability. For contributors only.', 'woocommerce' ),
'<strong>',
'</strong>'
),
);
if ( $this->data_synchronizer->check_orders_table_exists() ) { if ( $this->data_synchronizer->check_orders_table_exists() ) {
$settings[] = $title_item; $settings[] = array(
'title' => __( 'Custom orders tables', 'woocommerce' ),
'type' => 'title',
'id' => 'cot-title',
'desc' => sprintf(
/* translators: %1$s = <strong> tag, %2$s = </strong> tag. */
__( '%1$sWARNING:%2$s This feature is currently under development and may cause database instability. For contributors only.', 'woocommerce' ),
'<strong>',
'</strong>'
),
);
$sync_status = $this->data_synchronizer->get_sync_status();
$sync_is_pending = 0 !== $sync_status['current_pending_count'];
$settings[] = array( $settings[] = array(
'title' => __( 'Enable tables usage', 'woocommerce' ), 'title' => __( 'Data store for orders', 'woocommerce' ),
'desc' => __( 'Use the custom orders tables as the main orders data store.', 'woocommerce' ),
'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
'default' => 'no', 'default' => 'no',
'type' => 'checkbox', 'type' => 'radio',
'options' => array(
'yes' => __( 'Use the WooCommerce orders tables', 'woocommerce' ),
'no' => __( 'Use the WordPress posts table', 'woocommerce' ),
),
'checkboxgroup' => 'start', 'checkboxgroup' => 'start',
'disabled' => $sync_is_pending ? array( 'yes', 'no' ) : array(),
); );
if ( $sync_is_pending ) {
$initial_pending_count = $sync_status['initial_pending_count'];
if ( $initial_pending_count ) {
$text = sprintf(
/* translators: %1$s=current number of orders pending sync, %2$s=initial number of orders pending sync */
__( 'There are %1$s orders (out of a total of %2$s) pending sync!', 'woocommerce' ),
$sync_status['current_pending_count'],
$initial_pending_count
);
} else {
$text = sprintf(
/* translators: %1$s=current number of orders pending sync */
__( 'There are %1$s orders pending sync!', 'woocommerce' ),
$sync_status['current_pending_count']
);
}
if ( $this->data_synchronizer->pending_data_sync_is_in_progress() ) {
$text .= __( '<br/>Syncrhonization for these orders is currently in progress.', 'woocommerce' );
}
$text .= __( "<br/>The authoritative table can't be changed until sync completes.", 'woocommerce' );
$settings[] = array(
'type' => 'info',
'id' => 'cot-out-of-sync-warning',
'css' => 'color: #C00000',
'text' => $text,
);
}
$settings[] = array(
'desc' => __( 'Keep the posts table and the orders tables synchronized', 'woocommerce' ),
'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
'type' => 'checkbox',
);
if ( $sync_is_pending ) {
if ( $this->data_synchronizer->data_sync_is_enabled() ) {
$message = $this->custom_orders_table_usage_is_enabled() ?
__( 'Switch to using the posts table as the authoritative data store for orders when sync finishes', 'woocommerce' ) :
__( 'Switch to using the orders table as the authoritative data store for orders when sync finishes', 'woocommerce' );
$settings[] = array(
'desc' => $message,
'id' => DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION,
'type' => 'checkbox',
);
}
}
} else { } else {
$title_item['desc'] = sprintf( $settings[] = array(
/* translators: %1$s = <em> tag, %2$s = </em> tag. */ 'title' => __( 'Custom orders tables', 'woocommerce' ),
__( 'Create the tables first by going to %1$sWooCommerce > Status > Tools%2$s and running %1$sCreate the custom orders tables%2$s.', 'woocommerce' ), 'type' => 'title',
'<em>', 'desc' => sprintf(
'</em>' /* translators: %1$s = <em> tag, %2$s = </em> tag. */
__( 'Create the tables first by going to %1$sWooCommerce > Status > Tools%2$s and running %1$sCreate the custom orders tables%2$s.', 'woocommerce' ),
'<em>',
'</em>'
),
); );
$settings[] = $title_item;
} }
$settings[] = array( 'type' => 'sectionend' ); $settings[] = array( 'type' => 'sectionend' );
return $settings; return $settings;
} }
/**
* Handler for the individual setting updated hook.
*
* @param string $option Setting name.
* @param mixed $old_value Old value of the setting.
* @param mixed $value New value of the setting.
*/
private function process_updated_option( $option, $old_value, $value ) {
if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION === $option && 'no' === $value ) {
$this->data_synchronizer->cleanup_synchronization_state();
}
}
/**
* Handler for the setting pre-update hook.
* We use it to verify that authoritative orders table switch doesn't happen while sync is pending.
*
* @param string $option Setting name.
* @param mixed $old_value Old value of the setting.
* @param mixed $value New value of the setting.
*
* @throws \Exception Attempt to change the authoritative orders table while orders sync is pending.
*/
private function process_pre_update_option( $option, $old_value, $value ) {
if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $option || $value === $old_value || false === $old_value ) {
return $value;
}
$sync_is_pending = 0 !== $this->data_synchronizer->get_current_orders_pending_sync_count();
if ( $sync_is_pending ) {
throw new \Exception( "The authoritative table for orders storage can't be changed while there are orders out of sync" );
}
return $value;
}
/**
* Handler for the synchronization finished hook.
* Here we switch the authoritative table if needed.
*/
private function process_sync_finished() {
if ( $this->auto_flip_authoritative_table_enabled() ) {
return;
}
update_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' );
if ( $this->custom_orders_table_usage_is_enabled() ) {
update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' );
} else {
update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'yes' );
}
}
/**
* Is the automatic authoritative table switch setting set?
*
* @return bool
*/
private function auto_flip_authoritative_table_enabled(): bool {
return 'yes' === get_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION );
}
/**
* Handler for the all settings updated hook.
*/
private function process_options_updated() {
$data_sync_is_enabled = $this->data_synchronizer->data_sync_is_enabled();
// Disabling the sync implies disabling the automatic authoritative table switch too.
if ( ! $data_sync_is_enabled && $this->auto_flip_authoritative_table_enabled() ) {
update_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' );
}
// Enabling the sync implies starting it too, if needed.
// We do this check here, and not in process_pre_update_option, so that if for some reason
// the setting is enabled but no sync is in process, sync will start by just saving the
// settings even without modifying them.
if ( $data_sync_is_enabled && ! $this->data_synchronizer->pending_data_sync_is_in_progress() ) {
$this->data_synchronizer->start_synchronizing_pending_orders();
}
}
} }

View File

@ -17,6 +17,13 @@ defined( 'ABSPATH' ) || exit;
*/ */
class DataSynchronizer { class DataSynchronizer {
const ORDERS_DATA_SYNC_ENABLED_OPTION = 'woocommerce_custom_orders_table_data_sync_enabled';
const INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION = 'woocommerce_initial_orders_pending_sync_count';
const AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION = 'woocommerce_auto_flip_authoritative_table_roles';
const PENDING_SYNC_IS_IN_PROGRESS_OPTION = 'woocommerce_custom_orders_table_pending_sync_in_progress';
const ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK = 'woocommerce_run_orders_sync_callback';
const PENDING_SYNCHRONIZATION_FINISHED_ACTION = 'woocommerce_orders_sync_finished';
/** /**
* The data store object to use. * The data store object to use.
* *
@ -31,7 +38,17 @@ class DataSynchronizer {
*/ */
private $database_util; private $database_util;
// TODO: Add a constructor to handle hooks as appropriate. /**
* Class constructor.
*/
public function __construct() {
add_action(
self::ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK,
function() {
$this->do_pending_orders_synchronization();
}
);
}
/** /**
* Class initialization, invoked by the DI container. * Class initialization, invoked by the DI container.
@ -74,5 +91,113 @@ class DataSynchronizer {
} }
} }
/**
* Is the data sync between old and new tables currently enabled?
*
* @return bool
*/
public function data_sync_is_enabled(): bool {
return 'yes' === get_option( self::ORDERS_DATA_SYNC_ENABLED_OPTION );
}
/**
* Is a sync process currently in progress?
*
* @return bool
*/
public function pending_data_sync_is_in_progress(): bool {
return 'yes' === get_option( self::PENDING_SYNC_IS_IN_PROGRESS_OPTION );
}
/**
* Get the current sync process status.
* The information is meaningful only if pending_data_sync_is_in_progress return true.
*
* @return array
*/
public function get_sync_status() {
return array(
'initial_pending_count' => (int) get_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION, 0 ),
'current_pending_count' => $this->get_current_orders_pending_sync_count(),
'auto_flip' => 'yes' === get_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION ),
'sync_in_progress' => $this->pending_data_sync_is_in_progress(),
);
}
/**
* Calculate how many orders need to be synchronized currently.
*/
public function get_current_orders_pending_sync_count(): int {
// TODO: get this value by querying the database.
return get_option( 'woocommerce_fake_orders_pending_sync_count', 0 );
}
/**
* Start an orders synchronization process.
* This will setup the appropriate status information and schedule the first synchronization batch.
*/
public function start_synchronizing_pending_orders() {
$initial_pending_count = $this->get_current_orders_pending_sync_count();
if ( 0 === $initial_pending_count ) {
return;
}
update_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION, $initial_pending_count );
$queue = WC()->get_instance_of( \WC_Queue::class );
$queue->cancel_all( self::ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK );
update_option( self::PENDING_SYNC_IS_IN_PROGRESS_OPTION, 'yes' );
$this->schedule_pending_orders_synchronization();
}
/**
* Schedule the next orders synchronization batch.
*/
private function schedule_pending_orders_synchronization() {
$queue = WC()->get_instance_of( \WC_Queue::class );
$queue->schedule_single(
WC()->call_function( 'time' ) + 1,
self::ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK,
array(),
'woocommerce-db-updates'
);
}
/**
* Run one orders synchronization batch.
*/
private function do_pending_orders_synchronization() {
if ( ! $this->pending_data_sync_is_in_progress() ) {
return;
}
// TODO: Syncrhonize a batch of orders.
$fake_count = (int) get_option( 'woocommerce_fake_orders_pending_sync_count', 0 );
if ( $fake_count > 0 ) {
update_option( 'woocommerce_fake_orders_pending_sync_count', $fake_count - 1 );
}
if ( 0 === $this->get_current_orders_pending_sync_count() ) {
$this->cleanup_synchronization_state();
/**
* Hook to signal that the orders tables synchronization process has finised (nothing left to synchronize).
*/
do_action( self::PENDING_SYNCHRONIZATION_FINISHED_ACTION );
} else {
$this->schedule_pending_orders_synchronization();
}
}
/**
* Cleanup all the synchronization status information,
* because the process has been disabled by the user via settings,
* or because there's nothing left to syncrhonize.
*/
public function cleanup_synchronization_state() {
delete_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION );
delete_option( self::PENDING_SYNC_IS_IN_PROGRESS_OPTION );
delete_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION );
}
} }