File: /home/xedaptot/ai.naniguide.com/app/Jobs/RefreshCacheableJob.php
<?php
namespace App\Jobs;
use App\Library\Cache\AppCache;
use App\Library\Cache\Cacheable;
use App\Notifications\System\CacheRefreshFailed;
use App\Services\Notifications\Notifier;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class RefreshCacheableJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public int $uniqueFor = 300;
public int $tries = 1;
public int $maxExceptions = 1;
/**
* Worker SIGALRM kicks in after this many seconds (Laravel respects $timeout
* on the queue worker; jobs override the worker default).
*
* Per-entry jobs run one cache closure each; the slowest known closure today
* is `Campaign::bounceCount()` at ~50s on 100k+ recipient campaigns. 600s
* (10 min) leaves headroom for future heavier aggregates without letting a
* runaway query lock a worker indefinitely.
*/
public int $timeout = 600;
public function __construct(
public Cacheable $cacheable,
public ?string $entry = null,
) {
}
public function uniqueId(): string
{
return 'cache:' . $this->cacheable->cacheKey() . ':' . ($this->entry ?? '*');
}
/**
* Dispatch one job per cache entry (fan-out).
*
* A single all-entries job runs every closure in one process; on a slow
* worker the long entries (e.g. 50s bounce-count joins) can push past the
* worker timeout and leave the trailing entries un-warmed (the freshness
* strip then renders zeros forever until the user manually refreshes).
* Per-entry jobs bound each entry's runtime, and a failure on one entry
* stops only that entry — the rest still complete.
*/
public static function dispatchAll(Cacheable $cacheable): void
{
$scope = AppCache::for($cacheable);
foreach ($scope->entryNames() as $name) {
self::dispatch($cacheable, $name);
}
}
public function handle(): void
{
AppCache::for($this->cacheable)->refresh($this->entry);
}
/**
* Worker calls this when handle() throws OR when $timeout is exceeded
* (SIGALRM kills the process and the framework treats it as a failure
* once $tries is reached — which is 1, so first failure surfaces).
*
* Side effect by design: log + admin notification. Without this, a silently
* failing refresh leaves the freshness strip stuck on stale or zero values
* with no breadcrumb for support.
*/
public function failed(\Throwable $e): void
{
$cacheKey = $this->cacheable->cacheKey();
$entry = $this->entry ?? '*';
Log::error('RefreshCacheableJob failed', [
'cache_key' => $cacheKey,
'entry' => $entry,
'error' => $e->getMessage(),
]);
app(Notifier::class)->shareWithAdmins(
new CacheRefreshFailed($cacheKey, $this->entry, $e)
);
}
}