artworkId = $artworkId; $this->count = max(1, $count); $this->eventId = $eventId; } /** * Execute the job. * Uses Redis setnx to ensure only one worker processes a given eventId. * Delegates actual DB mutation to ArtworkStatsService which uses transactions. */ public function handle(ArtworkStatsService $statsService): void { $key = 'artwork:view:processed:' . $this->eventId; try { $didSet = false; try { $didSet = Redis::setnx($key, 1); if ($didSet) { // expire after 1 day to limit key growth Redis::expire($key, 86400); } } catch (\Throwable $e) { Log::warning('Redis unavailable for IncrementArtworkView; proceeding without dedupe', ['error' => $e->getMessage()]); // If Redis is not available, fall back to applying delta directly. // This sacrifices idempotency but ensures metrics are recorded. $statsService->applyDelta($this->artworkId, ['views' => $this->count]); return; } if (! $didSet) { // Already processed this eventId — idempotent skip return; } // Safe increment using transactional method $statsService->applyDelta($this->artworkId, ['views' => $this->count]); } catch (\Throwable $e) { Log::error('IncrementArtworkView job failed', ['artwork_id' => $this->artworkId, 'event_id' => $this->eventId, 'error' => $e->getMessage()]); // Let the job be retried by throwing throw $e; } } }