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();
}
}