From 802fc5c4413706eedbbca8b2209e7b850dcf07ef Mon Sep 17 00:00:00 2001 From: Arnaud Hours Date: Tue, 30 Sep 2025 17:05:20 +0200 Subject: [PATCH] fix(import): add mysql lock, add transaction in order creation --- includes/class-lengow-import-order.php | 23 ++- includes/class-lengow-import.php | 196 +++++++++++++++---------- 2 files changed, 134 insertions(+), 85 deletions(-) diff --git a/includes/class-lengow-import-order.php b/includes/class-lengow-import-order.php index b16b4c79..8e4030e1 100755 --- a/includes/class-lengow-import-order.php +++ b/includes/class-lengow-import-order.php @@ -897,6 +897,11 @@ private function can_create_order_shipped_by_marketplace() { * @return boolean */ private function create_order() { + global $wpdb; + + $wpdb->query( 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED' ); // optional + $wpdb->query( 'START TRANSACTION' ); + try { // search and get all products. $products = $this->get_products(); @@ -961,15 +966,25 @@ private function create_order() { $this->log_output, $this->marketplace_sku ); + + $wpdb->query( 'COMMIT' ); + + do_action( 'woocommerce_new_order', $order->get_id(), $order ); + $this->notify_admin_new_order( $order->get_id() ); + $this->notify_customer_received_order( $order->get_id() ); } catch ( Lengow_Exception $e ) { + $wpdb->query( 'ROLLBACK' ); $error_message = $e->getMessage(); } catch ( Exception $e ) { + $wpdb->query( 'ROLLBACK' ); $error_message = '[WooCommerce error]: "' . $e->getMessage() . '" in ' . $e->getFile() . ' on line ' . $e->getLine(); } + if ( ! isset( $error_message ) ) { return true; } + Lengow_Order::add_order_error( $this->order_lengow_id, $error_message ); $decoded_message = Lengow_Main::decode_log_message( $error_message, Lengow_Translation::DEFAULT_ISO_CODE ); $this->errors[] = $decoded_message; @@ -1253,7 +1268,7 @@ private function create_woocommerce_order( $user, $products, $billing_address, $ // create a generic order. add_filter( 'woocommerce_email_enabled_new_order', '__return_false' ); - add_filter('woocommerce_email_enabled_customer_on_hold_order', '__return_false'); + add_filter( 'woocommerce_email_enabled_customer_on_hold_order', '__return_false' ); $wc_order = $this->create_generic_woocommerce_order(); $order_id = $wc_order->get_id(); // get billing data formatted for WooCommerce address. @@ -1330,12 +1345,8 @@ private function create_woocommerce_order( $user, $products, $billing_address, $ // add quantity back for re-import order and order shipped by marketplace. $this->add_quantity_back( $wc_order ); remove_filter( 'woocommerce_email_enabled_new_order', '__return_false' ); - remove_filter('woocommerce_email_enabled_customer_on_hold_order', '__return_false'); + remove_filter( 'woocommerce_email_enabled_customer_on_hold_order', '__return_false' ); $wc_order->save(); - do_action( 'woocommerce_new_order', $wc_order->get_id(), $wc_order ); - $this->notify_admin_new_order( $wc_order->get_id() ); - $this->notify_customer_received_order( $wc_order->get_id() ); - return $wc_order; } diff --git a/includes/class-lengow-import.php b/includes/class-lengow-import.php index 43b5e0c0..417891c9 100755 --- a/includes/class-lengow-import.php +++ b/includes/class-lengow-import.php @@ -116,11 +116,6 @@ class Lengow_Import { Lengow_Order::STATE_CLOSED, ); - /** - * @var boolean import is processing. - */ - public static $processing; - /** * @var string|null marketplace order sku. */ @@ -242,6 +237,21 @@ class Lengow_Import { */ private $errors = array(); + /** + * @var string|null DB lock name + */ + private $db_lock_name = null; + + /** + * @var int seconds to wait to acquire DB lock + */ + private $db_lock_wait = 5; + + /** + * @var bool whether we acquired the DB lock + */ + private $db_lock_acquired = false; + /** * Construct the import manager. * @@ -309,42 +319,74 @@ public function __construct( $params = array() ) { * @return array */ public function exec() { + global $wpdb; + $sync_ok = true; - // checks if a synchronization is not already in progress. - if ( ! $this->can_execute_synchronization() ) { - return $this->get_result(); - } - // starts some processes necessary for synchronization. - $this->setup_synchronization(); - // synchronize all orders for a specific shop - if ( Lengow_Configuration::get( Lengow_Configuration::SHOP_ACTIVE ) && ! $this->synchronize_orders_by_shop() ) { - $sync_ok = false; + + $site_id = function_exists( 'get_current_blog_id' ) ? get_current_blog_id() : 0; + if ( $this->import_one_order && ! empty( $this->marketplace_sku ) ) { + $lock_name = sprintf( 'lengow_import_%d_order_%s', $site_id, $this->marketplace_sku ); + } else { + $lock_name = sprintf( 'lengow_import_%d_global', $site_id ); } - // get order synchronization result - $result = $this->get_result(); - Lengow_Main::log( - Lengow_Log::CODE_IMPORT, - Lengow_Main::set_log_message( - 'log.import.sync_result', - array( - 'number_orders_processed' => $result[ self::NUMBER_ORDERS_PROCESSED ], - 'number_orders_created' => $result[ self::NUMBER_ORDERS_CREATED ], - 'number_orders_updated' => $result[ self::NUMBER_ORDERS_UPDATED ], - 'number_orders_failed' => $result[ self::NUMBER_ORDERS_FAILED ], - 'number_orders_ignored' => $result[ self::NUMBER_ORDERS_IGNORED ], - 'number_orders_not_formatted' => $result[ self::NUMBER_ORDERS_NOT_FORMATTED ], - ) - ), - $this->log_output - ); - // update last synchronization date only if importation succeeded. - if ( ! $this->import_one_order && $sync_ok ) { - Lengow_Main::update_date_import( $this->type_import ); + $this->db_lock_name = $lock_name; + + $wait = $this->db_lock_wait; + $got = (int) $wpdb->get_var( $wpdb->prepare( "SELECT GET_LOCK(%s, %d)", $lock_name, $wait ) ); + + if ( $got !== 1 ) { + $message = Lengow_Main::set_log_message( + 'lengow_log.error.rest_time_to_import', + array( 'rest_time' => $wait ) + ); + Lengow_Main::log( Lengow_Log::CODE_IMPORT, $message, $this->log_output ); + return $this->get_result(); } - // complete synchronization and start all necessary processes. - $this->finish_synchronization(); - return $result; + $this->db_lock_acquired = true; + + try { + if ( ! $this->can_execute_synchronization() ) { + return $this->get_result(); + } + // starts some processes necessary for synchronization. + $this->setup_synchronization(); + // synchronize all orders for a specific shop + if ( Lengow_Configuration::get( Lengow_Configuration::SHOP_ACTIVE ) && ! $this->synchronize_orders_by_shop() ) { + $sync_ok = false; + } + // get order synchronization result + $result = $this->get_result(); + Lengow_Main::log( + Lengow_Log::CODE_IMPORT, + Lengow_Main::set_log_message( + 'log.import.sync_result', + array( + 'number_orders_processed' => $result[ self::NUMBER_ORDERS_PROCESSED ], + 'number_orders_created' => $result[ self::NUMBER_ORDERS_CREATED ], + 'number_orders_updated' => $result[ self::NUMBER_ORDERS_UPDATED ], + 'number_orders_failed' => $result[ self::NUMBER_ORDERS_FAILED ], + 'number_orders_ignored' => $result[ self::NUMBER_ORDERS_IGNORED ], + 'number_orders_not_formatted' => $result[ self::NUMBER_ORDERS_NOT_FORMATTED ], + ) + ), + $this->log_output + ); + // update last synchronization date only if importation succeeded. + if ( ! $this->import_one_order && $sync_ok ) { + Lengow_Main::update_date_import( $this->type_import ); + } + // complete synchronization and start all necessary processes. + $this->finish_synchronization(); + + return $result; + } finally { + if ( $this->db_lock_acquired && ! empty( $this->db_lock_name ) ) { + $wpdb->get_var( $wpdb->prepare( "SELECT RELEASE_LOCK(%s)", $this->db_lock_name ) ); + $this->db_lock_acquired = false; + $this->db_lock_name = null; + } + } } public function getLogsDisplay() { @@ -374,18 +416,27 @@ public static function check_state( $order_state_marketplace, $marketplace ) { /** * Check if order synchronization is already in process. * + * @param string|null $marketplace_sku Optional marketplace SKU to check a per-order lock. + * * @return boolean */ - public static function is_in_process() { - $timestamp = (int) Lengow_Configuration::get( Lengow_Configuration::SYNCHRONIZATION_IN_PROGRESS ); - if ( $timestamp > 0 ) { - // security check: if last import is more than 60 seconds old => authorize new import to be launched. - if ( ( $timestamp + ( 60 * self::MINUTE_INTERVAL_TIME ) ) < time() ) { - self::set_end(); + public static function is_in_process( $marketplace_sku = null ) { + global $wpdb; - return false; - } + $site_id = function_exists( 'get_current_blog_id' ) ? get_current_blog_id() : 0; + if ( ! empty( $marketplace_sku ) ) { + $lock_name = sprintf( 'lengow_import_%d_order_%s', $site_id, $marketplace_sku ); + } else { + $lock_name = sprintf( 'lengow_import_%d_global', $site_id ); + } + + try { + $owner = $wpdb->get_var( $wpdb->prepare( "SELECT IS_USED_LOCK(%s)", $lock_name ) ); + } catch ( Throwable $e ) { + $owner = null; + } + if ( $owner !== null ) { return true; } @@ -395,12 +446,28 @@ public static function is_in_process() { /** * Get Rest time to make a new order synchronization. * + * @param string|null $marketplace_sku Optional marketplace SKU to check per-order lock + * * @return integer */ - public static function rest_time_to_import() { - $timestamp = (int) Lengow_Configuration::get( Lengow_Configuration::SYNCHRONIZATION_IN_PROGRESS ); - if ( $timestamp > 0 ) { - return $timestamp + ( 60 * self::MINUTE_INTERVAL_TIME ) - time(); + public static function rest_time_to_import( $marketplace_sku = null ) { + global $wpdb; + + $site_id = function_exists( 'get_current_blog_id' ) ? get_current_blog_id() : 0; + if ( ! empty( $marketplace_sku ) ) { + $lock_name = sprintf( 'lengow_import_%d_order_%s', $site_id, $marketplace_sku ); + } else { + $lock_name = sprintf( 'lengow_import_%d_global', $site_id ); + } + + try { + $owner = $wpdb->get_var( $wpdb->prepare( "SELECT IS_USED_LOCK(%s)", $lock_name ) ); + } catch ( Throwable $e ) { + $owner = null; + } + + if ( $owner !== null ) { + return 60; // factice value } return 0; @@ -463,13 +530,7 @@ private function set_interval_time( $days = null, $created_from = null, $created */ private function can_execute_synchronization() { $global_error = false; - if ( ! $this->debug_mode && ! $this->import_one_order && self::is_in_process() ) { - $global_error = Lengow_Main::set_log_message( - 'lengow_log.error.rest_time_to_import', - array( 'rest_time' => self::rest_time_to_import() ) - ); - Lengow_Main::log( Lengow_Log::CODE_IMPORT, $global_error, $this->log_output ); - } elseif ( ! $this->check_credentials() ) { + if ( ! $this->check_credentials() ) { $global_error = Lengow_Main::set_log_message( 'lengow_log.error.credentials_not_valid' ); Lengow_Main::log( Lengow_Log::CODE_IMPORT, $global_error, $this->log_output ); } @@ -493,9 +554,6 @@ private function can_execute_synchronization() { private function setup_synchronization() { // suppress log files when too old. Lengow_Main::clean_log(); - if ( ! $this->import_one_order ) { - self::set_in_process(); - } // check Lengow catalogs for order synchronisation. if ( ! $this->import_one_order && self::TYPE_MANUAL === $this->type_import ) { Lengow_Sync::sync_catalog(); @@ -804,9 +862,6 @@ private function get_orders_from_api() { private function import_orders( $orders ) { $import_finished = false; foreach ( $orders as $order_data ) { - if ( ! $this->import_one_order ) { - self::set_in_process(); - } $nb_package = 0; $marketplace_sku = (string) $order_data->marketplace_order_id; if ( $this->debug_mode ) { @@ -971,7 +1026,6 @@ private function save_synchronization_result( $result ) { */ private function finish_synchronization() { // finish synchronization process. - self::set_end(); Lengow_Main::log( Lengow_Log::CODE_IMPORT, Lengow_Main::set_log_message( 'log.import.end', array( 'type' => $this->type_import ) ), @@ -991,20 +1045,4 @@ private function finish_synchronization() { Lengow_Main::send_mail_alert( $this->log_output ); } } - - /** - * Set import to "in process" state. - */ - private static function set_in_process() { - self::$processing = true; - Lengow_Configuration::update_value( Lengow_Configuration::SYNCHRONIZATION_IN_PROGRESS, time() ); - } - - /** - * Set import to finished. - */ - private static function set_end() { - self::$processing = false; - Lengow_Configuration::update_value( Lengow_Configuration::SYNCHRONIZATION_IN_PROGRESS, - 1 ); - } }