0). * * @throws InvalidArgumentException When `$batchSize <= 0`. */ public function __construct( StaleProductFinder $finder, SyncPolicy $policy, SyncDispatcher $dispatcher, Clock $clock, int $batchSize = self::DEFAULT_BATCH_SIZE ) { if ( $batchSize <= 0 ) { $message = sprintf( 'PeriodicSyncScheduler requires batchSize > 0, got %d.', $batchSize ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } $this->finder = $finder; $this->policy = $policy; $this->dispatcher = $dispatcher; $this->clock = $clock; $this->batchSize = $batchSize; } /** * Per-tick enqueue cap. */ public function batchSize(): int { return $this->batchSize; } /** * Sweep the stale cohort and enqueue one `sync_product` job per * record, returning the resulting tick summary. * * Stamps a single UTC "now" instant at the top of the tick, asks * the finder for at most {@see self::$batchSize} records, and * routes each record through the dispatcher at the priority the * policy attributes to it. Records without a persisted id are * skipped defensively (contract violation by the finder); they * are counted on the result so the surrounding scheduler can * surface the anomaly. */ public function tick(): PeriodicSyncTickResult { $now = $this->clock->now()->setTimezone( new DateTimeZone( 'UTC' ) ); $records = $this->finder->findStale( $this->batchSize ); $enqueued = 0; $skipped = 0; $enqueuedIds = array(); foreach ( $records as $record ) { if ( ! $record instanceof ProductRecord ) { ++$skipped; continue; } $id = $record->id(); if ( $id === null || $id <= 0 ) { ++$skipped; continue; } $priority = $this->policy->priorityFor( $record ); $this->dispatcher->enqueueProductSync( $id, $priority ); ++$enqueued; $enqueuedIds[] = $id; } return new PeriodicSyncTickResult( count( $records ), $enqueued, $skipped, $enqueuedIds, $now ); } }