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/BulkVerify.php
<?php

namespace App\Jobs;

use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Library\Traits\Trackable;
use App\Library\Exception\RateLimitExceeded;
use App\Library\BulkCreditTracker;
use App\Library\Contracts\CreditTrackerInterface;
use App\Library\Contracts\BulkVerificationTargetInterface;
use App\Model\EmailVerificationServer;
use Exception;

class BulkVerify implements ShouldQueue
{
    use Trackable;
    use Batchable;
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public $timeout = 900;
    public $maxExceptions = 1; // This is required if retryUntil is used, otherwise, the default value is 255
    public $failOnTimeout = true;

    public const MIN_PER_BATCH = 200;
    public const MAX_PER_BATCH = 1000;
    public const NUMBER_OF_BATCHES = 10;

    // Allow it to try MANY times (in case of rate limit exceed)
    // public $tries = 1;

    protected $server;
    protected $list;
    protected $customer;

    /**
     * The credit tracker to deduct verification credits from, or null if unlimited / no subscription.
     *
     * SERIALIZATION REQUIREMENT: This tracker must be stateless from the object's perspective —
     * its mutable state must live outside the object (e.g. on disk for CreditTracker, in cache for
     * InMemoryCreditTracker). When Laravel serializes this job to the queue and later deserializes it,
     * the tracker is reconstructed from its serialized fields (filepath / resourceKey). Any operation
     * (test, count, rollback) then reads/writes the external store directly, so the values are always
     * current regardless of when deserialization happened.
     *
     * DO NOT pass an object that holds live credit state purely in a PHP property — it would be
     * snapshotted at dispatch time and silently stale when the job eventually runs.
     */
    protected ?CreditTrackerInterface $creditTracker;

    public function __construct(BulkVerificationTargetInterface $list, EmailVerificationServer $server, ?CreditTrackerInterface $creditTracker = null)
    {
        $this->list = $list;
        $this->server = $server;
        $this->creditTracker = $creditTracker;

        $this->customer = $this->list->customer;
    }

    /**
     * Determine the time at which the job should timeout.
     *
     * @return \DateTime
     */
    public function retryUntil()
    {
        // @important: remember that messages might be released over and over
        // if there is any limit setting in place
        // As a result, it is just save to have it retry virtually forever
        return now()->addDays(7);
    }

    public function handle()
    {
        $this->customer->setUserDbConnection();

        if ($this->batch()->cancelled()) {
            $this->list->logger()->warning('[BulkVerify] Job cancelled');
            return;
        }

        $this->monitor->updateJsonData([
            'percentage' => round($this->list->getVerifiedSubscribersPercentage() * 100, 2),
            'message' => trans('messages.list.verification.progress.started'),
        ]);

        $logger = $this->list->logger();
        $logger->info('[BulkVerify] Start bulk verifying');

        $creditTracker = $this->creditTracker;

        if (!is_null($creditTracker)) {
            // Redundant check — execute_with_limits will enforce this too, but fail fast here before doing any work
            $creditTracker->test();
        }

        $serviceCredits = $this->server->driver()->getCredits();
        if (!is_null($serviceCredits) && $serviceCredits <= 0) { // check null first, otherwise, null <= 0 always returns true
            $logger->warning('[BulkVerify] User has run out of service credit');
            throw new Exception('User  run out of service credit');
        }

        $subscribersQuery = $this->list->getUnverifiedQuery();
        $unverifiedCount = $subscribersQuery->count();
        if ($unverifiedCount == 0) {
            $this->monitor->updateJsonData([
                'message' => trans('messages.list.verification.progress.done'),
                'batch_info' => null, // clean up batch information
                'percentage' => 100,
            ]);

            $logger->info('[BulkVerify] No contact left, verification done');
            return;
        }

        $perSubmit = (int) floor($unverifiedCount / static::NUMBER_OF_BATCHES);

        if ($perSubmit < static::MIN_PER_BATCH) {
            $perSubmit = static::MIN_PER_BATCH;
        } elseif ($perSubmit > static::MAX_PER_BATCH) {
            $perSubmit = static::MAX_PER_BATCH;
        }

        if ($unverifiedCount < $perSubmit) {
            $perSubmit = $unverifiedCount;
        }

        if (!is_null($creditTracker) && !$creditTracker->isUnlimited()) {
            $remainingCredits = $creditTracker->getRemainingCredits();
            if ($remainingCredits >= $perSubmit) {
                $logger->info("[BulkVerify] Remaining credits is {$remainingCredits}, ok to take {$perSubmit}");
            } else {
                $logger->info("[BulkVerify] Remaining credits is only {$remainingCredits}!");
                $perSubmit = $remainingCredits;
            }
        } else {
            $this->list->logger()->info("[BulkVerify] Good news, we have unlimited verification credits. Taking {$perSubmit} per batch");
        }

        // Sometimes it is null as the service does not support fetching remaining credits
        if (!is_null($serviceCredits) && $serviceCredits < $perSubmit) {
            $perSubmit = $serviceCredits;
            $logger->info("[BulkVerify] Available service credit is only {$serviceCredits}! So, only take {$serviceCredits}!");
        } else {
            $logger->info("[BulkVerify] Available service credit is {$serviceCredits}! ok to take {$perSubmit}");
        }

        $subscribersQuery = $subscribersQuery->limit($perSubmit);

        try {
            // Submit a partition of list to the remote verification service
            // Get the session token or id, use it to check status
            $trackers = !is_null($creditTracker) ? [new BulkCreditTracker($creditTracker, $perSubmit)] : [];
            $token = \App\Helpers\execute_with_limits([], null, $trackers, function () use ($subscribersQuery) {
                return $this->server->driver()->bulkSubmit($subscribersQuery);
            });
            $this->list->logger()->info("[BulkVerify] ==> Submitted! Waiting for BulkCheck job...");
            if (!is_null($creditTracker)) {
                $this->list->logger()->info("[BulkVerify] {$perSubmit} credits counted, {$creditTracker->getRemainingCredits()} remains ('-1' means UNLIMITED)");
            }

            sleep(1);

            // Dispatch checker job
            $bulkCheck = new BulkCheck($this->list, $this->server, $this->creditTracker, $token);
            $bulkCheck->setMonitor($this->monitor);
            $this->batch()->add($bulkCheck);
        } catch (RateLimitExceeded $ex) {
            $this->list->logger()->warning('[BulkVerify] Rate limit exceeded, try again in 10 seconds. '.$ex->getMessage());
            $this->release(10);
            return;
        } catch (\Throwable $ex) {
            // Laravel batch cannot catch Throwable errors (it is handled by higher level of Laravel)
            // Just catch Throwable here and throw Exception instead so that errors can be logged to job monitor
            throw new Exception('Verification failed. '.$ex->getMessage());
        }
    }
}