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/Services/Warmup/WarmupQuotaService.php
<?php

namespace App\Services\Warmup;

use Carbon\Carbon;
use Throwable;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use App\Model\SendingServer;
use App\Model\SendingServerWarmupLog;
use App\Model\SendingServerWarmupUsage;
use App\SendingServers\Drivers\SendResult;
use App\Services\Warmup\DTO\WarmupReservation;
use App\Services\Warmup\Exceptions\WarmupDailyQuotaExceeded;

class WarmupQuotaService
{
    public function __construct(
        protected WarmupStrategyFactory $strategyFactory
    ) {
    }

    /**
     * Main entry: send with warmup quota control
     */
    public function send(SendingServer $server, $message, array $context = []): SendResult
    {
        $reservation = $this->reserveOrFail($server, $context);

        try {
            $result = $server->send($message);
            if ($reservation) {
                $this->markSent($reservation, [
                    'result' => [
                        'status' => $result->getStatus()->value,
                        'runtime_message_id' => $result->getRuntimeMessageId(),
                    ],
                ]);
            }

            return $result;
        } catch (Throwable $e) {
            if ($reservation) {
                $this->markFailed($reservation, $e);
            }

            throw $e;
        }
    }

    /**
     * Day number (1-based)
     */
    public function getDayNumber(SendingServer $server, ?Carbon $date = null): int
    {
        $date ??= now();

        if (!$server->warmup_started_at) {
            return 1;
        }

        $startedAt = Carbon::parse($server->warmup_started_at)->startOfDay();
        $targetDate = $date->copy()->startOfDay();

        return max(1, $startedAt->diffInDays($targetDate) + 1);
    }

    /**
     * Calculate quota
     */
    public function getDailyQuota(SendingServer $server, ?Carbon $date = null): int
    {
        if (!$server->isWarmupEnabled()) {
            return PHP_INT_MAX;
        }

        $server->loadMissing('warmupStrategy');

        $strategy = $server->warmupStrategy;

        if (!$strategy) {
            return PHP_INT_MAX;
        }

        $dayNumber = $this->getDayNumber($server, $date);

        return $this->strategyFactory
            ->make($strategy)
            ->calculateDailyQuota($dayNumber);
    }

    /**
     * Remaining quota today
     */
    public function remainingQuota(SendingServer $server): ?int
    {
        if (!$server->isWarmupEnabled()) {
            return null;
        }

        $today = $this->today();
        $dayNumber = $this->getDayNumber($server);
        $dailyQuota = $this->getDailyQuota($server);

        $lockKey = sprintf('warmup:server:%s:%s', $server->id, $today);

        $usage = Cache::lock($lockKey, 5)->block(3, function () use ($server, $today, $dayNumber, $dailyQuota) {
            return DB::transaction(function () use ($server, $today, $dayNumber, $dailyQuota) {
                $usage = SendingServerWarmupUsage::query()
                    ->where('sending_server_id', $server->id)
                    ->where('usage_date', $today)
                    ->lockForUpdate()
                    ->first();

                if (!$usage) {
                    $usage = SendingServerWarmupUsage::create([
                        'sending_server_id' => $server->id,
                        'usage_date' => $today,
                        'day_number' => $dayNumber,
                        'daily_quota' => $dailyQuota,
                        'reserved_count' => 0,
                        'sent_count' => 0,
                        'failed_count' => 0,
                    ]);
                }

                return $usage;
            });
        });

        return max(
            0,
            (int) $usage->daily_quota - (int) $usage->reserved_count - (int) $usage->sent_count
        );
    }

    /**
     * Reserve slot or fail
     */
    public function reserveOrFail(SendingServer $server, array $context = []): ?WarmupReservation
    {
        if (!$server->isWarmupEnabled()) {
            return null;
        }

        $server->loadMissing('warmupStrategy');

        $today = $this->today();
        $dayNumber = $this->getDayNumber($server);
        $dailyQuota = $this->getDailyQuota($server);

        $lockKey = sprintf('warmup:server:%s:%s', $server->id, $today);

        return Cache::lock($lockKey, 5)->block(3, function () use ($server, $context, $today, $dayNumber, $dailyQuota) {
            return DB::transaction(function () use ($server, $context, $today, $dayNumber, $dailyQuota) {
                $usage = SendingServerWarmupUsage::query()
                    ->where('sending_server_id', $server->id)
                    ->where('usage_date', $today)
                    ->lockForUpdate()
                    ->first();

                if (!$usage) {
                    $usage = SendingServerWarmupUsage::create([
                        'sending_server_id' => $server->id,
                        'usage_date' => $today,
                        'day_number' => $dayNumber,
                        'daily_quota' => $dailyQuota,
                        'reserved_count' => 0,
                        'sent_count' => 0,
                        'failed_count' => 0,
                    ]);
                }

                $consumed = (int) $usage->reserved_count + (int) $usage->sent_count;

                if ($consumed >= (int) $usage->daily_quota) {
                    $retryAt = now()->addDay()->startOfDay()->addMinutes(1);

                    throw (new WarmupDailyQuotaExceeded(
                        sprintf(
                            'Warmup daily quota exceeded (server #%d, quota=%d)',
                            $server->id,
                            $usage->daily_quota
                        )
                    ))->setRetryAt($retryAt);
                }

                $usage->reserved_count++;
                $usage->last_reserved_at = now();
                $usage->save();

                $log = SendingServerWarmupLog::create([
                    'sending_server_id' => $server->id,
                    'message_id'        => $context['message_id'] ?? null,
                    'usage_date'        => $today,
                    'status'            => 'reserved',
                    'meta'              => $context,
                ]);

                return new WarmupReservation($usage->fresh(), $log);
            });
        });
    }

    /**
     * Mark success
     */
    public function markSent(WarmupReservation $reservation, array $meta = []): void
    {
        DB::transaction(function () use ($reservation, $meta) {
            $usage = SendingServerWarmupUsage::query()
                ->whereKey($reservation->getUsage()->id)
                ->lockForUpdate()
                ->firstOrFail();

            if ($usage->reserved_count > 0) {
                $usage->reserved_count--;
            }

            $usage->sent_count++;
            $usage->last_sent_at = now();
            $usage->save();

            $reservation->getLog()->update([
                'status' => 'sent',
                'meta' => array_merge((array) $reservation->getLog()->meta, $meta),
            ]);
        });
    }

    /**
     * Mark failed
     */
    public function markFailed(WarmupReservation $reservation, ?Throwable $exception = null, array $meta = []): void
    {
        DB::transaction(function () use ($reservation, $exception, $meta) {
            $usage = SendingServerWarmupUsage::query()
                ->whereKey($reservation->getUsage()->id)
                ->lockForUpdate()
                ->firstOrFail();

            if ($usage->reserved_count > 0) {
                $usage->reserved_count--;
            }

            $usage->failed_count++;
            $usage->save();

            if ($exception) {
                $meta['error'] = $exception->getMessage();
            }

            $reservation->getLog()->update([
                'status' => 'failed',
                'meta' => array_merge((array) $reservation->getLog()->meta, $meta),
            ]);
        });
    }

    protected function today(): string
    {
        return now()->toDateString();
    }
}