fix: run product sync in background batches to avoid 502 on large catalogs

Manual and scheduled sync previously processed every WooCommerce product
inline in a single PHP request/AJAX call, with several sequential BC API
calls per product. At real catalog sizes this exceeds PHP's
max_execution_time and the proxy's read timeout, surfacing as a 502 Bad
Gateway on "Sync Now".

Adds WBC_Batch_Sync, which queues the catalog in chunks of 25 via Action
Scheduler (bundled with WooCommerce) and processes them asynchronously.
The manual sync AJAX call now returns immediately after queuing, and the
admin UI polls for progress instead of waiting on one long request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 13:19:11 +02:00
parent 0aff5cf6d8
commit 5ac5c626f3
7 changed files with 353 additions and 107 deletions

View File

@@ -46,102 +46,37 @@ class WBC_Product_Sync {
);
/**
* Run the product sync (WooCommerce-first approach)
* Process a batch of WooCommerce products by ID (WooCommerce-first approach)
*
* Instead of pulling all 60k+ items from BC, we iterate WooCommerce
* products and query BC for each one by SKU/GTIN. This means ~100
* targeted API calls instead of 600+ paginated calls.
* products and query BC for each one by reference/GTIN/number. This is
* called per-batch by WBC_Batch_Sync so a single request never has to
* process the whole catalog (which would exceed PHP/proxy timeouts at
* real-world catalog sizes).
*
* @return array Sync results.
* @param int[] $product_ids WooCommerce product IDs to process.
* @return array Sync results for this batch.
*/
public function run_sync() {
// Check if sync is enabled
if ( get_option( 'wbc_enable_stock_sync', 'yes' ) !== 'yes' && get_option( 'wbc_enable_price_sync', 'yes' ) !== 'yes' ) {
WBC_Logger::info( 'ProductSync', 'Sync skipped - both stock and price sync are disabled' );
return array(
'success' => true,
'message' => __( 'Sync skipped - both stock and price sync are disabled.', 'woo-business-central' ),
);
}
public function run_sync_for_ids( array $product_ids ) {
foreach ( $product_ids as $product_id ) {
$product = wc_get_product( $product_id );
// Check if credentials are configured
if ( ! WBC_OAuth::is_configured() ) {
WBC_Logger::error( 'ProductSync', 'Sync failed - API credentials not configured' );
return array(
'success' => false,
'message' => __( 'Sync failed - API credentials not configured.', 'woo-business-central' ),
);
}
if ( ! $product || empty( $product->get_sku() ) ) {
continue;
}
WBC_Logger::info( 'ProductSync', 'Starting product sync (WooCommerce-first)' );
$start_time = microtime( true );
// Get all WooCommerce products that have a SKU
$products = $this->get_wc_products_with_sku();
if ( empty( $products ) ) {
WBC_Logger::info( 'ProductSync', 'No WooCommerce products with SKU found' );
return array(
'success' => true,
'message' => __( 'No WooCommerce products with SKU found.', 'woo-business-central' ),
);
}
WBC_Logger::info( 'ProductSync', 'Found WooCommerce products to sync', array(
'count' => count( $products ),
) );
// Process each WooCommerce product
foreach ( $products as $product ) {
$this->process_wc_product( $product );
// Small delay between API calls to avoid rate limiting
usleep( 50000 ); // 50ms
}
$duration = round( microtime( true ) - $start_time, 2 );
WBC_Logger::info( 'ProductSync', 'Product sync completed', array(
'duration_seconds' => $duration,
'total' => $this->results['total'],
'success' => $this->results['success'],
'failed' => $this->results['failed'],
'skipped' => $this->results['skipped'],
) );
return array(
'success' => empty( $this->results['errors'] ),
'message' => sprintf(
/* translators: 1: duration, 2: success count, 3: failed count, 4: skipped count */
__( 'Sync completed in %1$s seconds. Success: %2$d, Failed: %3$d, Skipped: %4$d', 'woo-business-central' ),
$duration,
$this->results['success'],
$this->results['failed'],
$this->results['skipped']
),
'results' => $this->results,
'success' => empty( $this->results['errors'] ),
'results' => $this->results,
);
}
/**
* Get all WooCommerce products that have a SKU set
*
* @return WC_Product[] Array of WooCommerce products.
*/
private function get_wc_products_with_sku() {
$products = wc_get_products( array(
'limit' => -1,
'status' => 'publish',
'return' => 'objects',
) );
// Filter to only products with SKUs
return array_filter( $products, function ( $product ) {
return ! empty( $product->get_sku() );
} );
}
/**
* Process a single WooCommerce product - find it in BC and sync
*