Automattic\WooCommerce\Internal\ProductFeed\Integrations\POSCatalog
AsyncGenerator::feed_generation_action │ public │ WC 10.5.0
Action scheduler callback for the feed generation.
Method of the class: AsyncGenerator{}
No Hooks.
Returns
null. Nothing (null).
Usage
$AsyncGenerator = new AsyncGenerator(); $AsyncGenerator->feed_generation_action( $option_key );
- $option_key(string) (required)
- The option key for the feed generation status.
Changelog
| Since 10.5.0 | Introduced. |
AsyncGenerator::feed_generation_action() AsyncGenerator::feed generation action code WC 11.0.0
public function feed_generation_action( string $option_key ) {
$status = get_option( $option_key );
// Only a scheduled (first chunk) or in-progress (continuation) job should be processed here.
if ( ! is_array( $status ) || ! in_array( $status['state'] ?? '', array( self::STATE_SCHEDULED, self::STATE_IN_PROGRESS ), true ) ) {
wc_get_logger()->error( 'Invalid feed generation status', array( 'status' => $status ) );
return;
}
$is_first_chunk = self::STATE_SCHEDULED === $status['state'];
// A continuation must know which feed file it is appending to. If it doesn't, the status is
// corrupt; bail and let the heartbeat-based recovery restart generation from scratch.
if ( ! $is_first_chunk && empty( $status['file_name'] ) ) {
wc_get_logger()->error( 'Invalid feed generation continuation status', array( 'status' => $status ) );
return;
}
$feed = null;
try {
$this->raise_resource_limits();
$feed = $this->integration->create_feed();
if ( $is_first_chunk ) {
$status['file_name'] = $feed->open();
$status['page'] = 1;
$status['processed'] = 0;
$status['entries_written'] = 0;
} else {
$feed->open( (string) $status['file_name'], (int) ( $status['entries_written'] ?? 0 ) );
}
// Only now that the feed lock is held (acquired by open()) do we claim the job as in-progress
// and refresh the heartbeat. Doing it earlier would rewrite the status — and bump updated_at —
// even for a run that immediately steps aside on FeedLockException, contradicting that path's
// "leave the status untouched" intent and refreshing the heartbeat of a job it never wrote.
$status['state'] = self::STATE_IN_PROGRESS;
$status['updated_at'] = time();
update_option( $option_key, $status );
$walker = ProductWalker::from_integration( $this->integration, $feed );
$walker->set_batch_size( $this->get_batch_size() );
$walker->add_time_limit( $this->get_batch_time_limit() );
$this->apply_mapper_args( $status['args'] ?? array() );
$start_page = max( 1, (int) ( $status['page'] ?? 1 ) );
$base_processed = (int) ( $status['processed'] ?? 0 );
$progress = $walker->walk_batches(
function ( WalkerProgress $progress ) use ( &$status, $option_key, $base_processed ) {
// Refresh progress and the heartbeat after every batch, so polling sees smooth progress
// within a chunk rather than a single jump at the chunk boundary.
$status = $this->update_progress( $status, $base_processed + $progress->processed_items, $progress->total_count );
update_option( $option_key, $status );
},
$start_page,
$this->get_chunk_batch_count( $option_key )
);
// The feed's entry count is already cumulative across chunks (open() seeds it with the running
// total when resuming), so store it as-is rather than adding to the previous total.
$status = $this->update_progress( $status, $base_processed + $progress->processed_items, $progress->total_count );
$status['entries_written'] = $feed->get_entry_count();
$status['page'] = $start_page + $progress->processed_batches;
$is_complete = $progress->total_batch_count <= 0 || (int) $status['page'] > $progress->total_batch_count;
if ( $is_complete ) {
$feed->end();
$status['state'] = self::STATE_COMPLETED;
$status['progress'] = 100;
$status['url'] = $feed->get_file_url();
$status['path'] = $feed->get_file_path();
$status['completed_at'] = time();
update_option( $option_key, $status );
// Schedule deletion of the file after the expiry time.
as_schedule_single_action(
time() + self::FEED_EXPIRY,
self::FEED_DELETION_ACTION,
array( $option_key, $feed->get_file_path() ),
'woo-product-feed',
false
);
} else {
$feed->flush();
update_option( $option_key, $status );
$this->schedule_generation_action( $option_key );
}
} catch ( FeedLockException $e ) {
// Another process already holds the feed file lock and is actively writing it, so this run
// is a redundant duplicate (e.g. Action Scheduler re-ran a slow chunk while the original was
// still in flight). Step aside and leave the status untouched: the lock holder is making
// progress, so marking the job failed here would report a failure for a healthy generation
// and let the next poll discard the partial file the holder is still writing. Release only
// this run's own handle (which never acquired the lock, so closing it cannot free the holder's).
if ( $feed instanceof ResumableFeedInterface ) {
$feed->flush();
}
return;
} catch ( \Throwable $e ) {
wc_get_logger()->error(
'Feed generation failed',
array(
'error' => $e->getMessage(),
'option_key' => $option_key,
)
);
// Release the file handle, if any, so it is not left dangling.
if ( $feed instanceof ResumableFeedInterface ) {
$feed->flush();
}
$status['state'] = self::STATE_FAILED;
$status['error'] = $e->getMessage();
$status['failed_at'] = time();
update_option( $option_key, $status );
}
}