Current state
This commit is contained in:
73
app/Jobs/IncrementArtworkView.php
Normal file
73
app/Jobs/IncrementArtworkView.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Services\ArtworkStatsService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class IncrementArtworkView implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $artworkId;
|
||||
public int $count;
|
||||
public string $eventId;
|
||||
|
||||
/**
|
||||
* Require a unique event id to make the job idempotent across retries and concurrency.
|
||||
*
|
||||
* @param int $artworkId
|
||||
* @param string $eventId Unique identifier for this view event (caller must supply)
|
||||
* @param int $count
|
||||
*/
|
||||
public function __construct(int $artworkId, string $eventId, int $count = 1)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user