can_sync() ) { WP_CLI::error( 'Market Sync is not connected. Restore the connection before resyncing the catalog.' ); } if ( ! function_exists( 'as_unschedule_all_actions' ) || ! function_exists( 'as_schedule_recurring_action' ) ) { WP_CLI::error( 'Action Scheduler is unavailable; the catalog was not reset.' ); } $job = new Catalog_Export_Job(); $action_id = 0; try { // Disable and fence the current lifecycle before canceling its pending // actions. A worker already in flight then cannot publish old data or // enqueue another child after this support reset. Catalog_Export_State::deactivate(); try { as_unschedule_all_actions( $job->hook() ); } finally { // Never leave catalog sync disabled if cancellation itself fails. // Activating also starts a new token and removes abandoned files. Catalog_Export_State::activate(); } Scheduler::invalidate_schedule_verification(); // One recurring action is enough: its first run is due immediately, // then Action Scheduler creates the next occurrence automatically. $action_id = (int) as_schedule_recurring_action( time(), $job->interval(), $job->hook(), [], $job->action_group(), false ); if ( $action_id <= 0 ) { throw new \RuntimeException( 'Action Scheduler rejected the catalog export.' ); } $runner = WP_CLI::runcommand( 'action-scheduler run', [ 'launch' => false, 'exit_error' => false, 'return' => 'all', 'command_args' => [ '--hooks=' . $job->hook(), '--batch-size=100', ], ] ); if ( ! is_object( $runner ) || 0 !== (int) ( $runner->return_code ?? 1 ) ) { $reason = is_object( $runner ) ? trim( (string) ( $runner->stderr ?? '' ) ) : ''; throw new \RuntimeException( '' !== $reason ? $reason : 'The catalog Action Scheduler runner failed.' ); } $failed_action = $this->failed_catalog_action_since( $job, $action_id ); if ( null !== $failed_action ) { throw new \RuntimeException( sprintf( 'Catalog %1$s action #%2$d failed; an automatic retry may be pending.', $failed_action['phase'], $failed_action['id'] ) ); } if ( null !== Catalog_Export_State::load() ) { throw new \RuntimeException( 'The catalog export is still active; an automatic retry may be pending.' ); } } catch ( Throwable $exception ) { Log_Helper::by_area( Log_Helper::LOG_CATALOG_EXPORT )->error( 'Manual catalog resync could not be completed.', [ 'reason' => $exception->getMessage() ] ); WP_CLI::error( 'Catalog resync failed: ' . $exception->getMessage() ); } Log_Helper::by_area( Log_Helper::LOG_CATALOG_EXPORT )->info( 'Manual catalog resync completed.', [ 'action_id' => $action_id ] ); WP_CLI::success( sprintf( 'Catalog reset complete; action #%d delivered the export. SaaS import continues asynchronously.', $action_id ) ); } /** * @return array{id: int, phase: string}|null */ private function failed_catalog_action_since( Catalog_Export_Job $job, int $root_action_id ): ?array { $actions = as_get_scheduled_actions( [ 'hook' => $job->hook(), 'group' => $job->action_group(), 'status' => \ActionScheduler_Store::STATUS_FAILED, 'per_page' => 100, 'orderby' => 'date', 'order' => 'DESC', ] ); foreach ( $actions as $action_id => $action ) { if ( (int) $action_id < $root_action_id ) { continue; } $args = $action->get_args(); $phase = isset( $args[0] ) && is_string( $args[0] ) ? $args[0] : 'root'; return [ 'id' => (int) $action_id, 'phase' => $phase, ]; } return null; } }