HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
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)
        );
    }
}