dpl/app/Services/MailFlow/QuotaService.php

85 lines
3.3 KiB
PHP

<?php
namespace App\Services\MailFlow;
use App\Models\MailMessage;
use App\Models\MailQuotaBucket;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class QuotaService
{
public function __construct(private ResendClient $client) {}
public function reserve(MailMessage $message): bool
{
if ($message->reservation) {
return true;
}
// One account on this website. Changing the API key does not grant extra quota.
$usage = Cache::remember('mailflow:resend:usage', 15, fn () => $this->client->usage());
foreach (['daily', 'monthly'] as $period) {
if (!isset($usage[$period]['used'], $usage[$period]['resets_at']) || !is_numeric($usage[$period]['used'])) {
throw new \RuntimeException('resend_usage_invalid');
}
}
return DB::transaction(function () use ($usage, $message) {
$buckets = [];
foreach (['daily', 'monthly'] as $period) {
$reset = Carbon::parse($usage[$period]['resets_at'])->setTimezone(config('app.timezone'));
if ($reset->isPast()) {
Cache::forget('mailflow:resend:usage');
throw new \RuntimeException('resend_usage_stale');
}
$limit = min((int) Settings::get($period . '_limit'), (int) ($usage[$period]['limit'] ?? Settings::get($period . '_limit')));
$key = $period . ':' . $reset->timestamp;
DB::table('v2_mail_quota_buckets')->insertOrIgnore([
'period_key' => $key, 'period' => $period, 'resets_at' => $reset,
'limit' => $limit, 'used' => 0, 'reserved' => 0,
'created_at' => now(), 'updated_at' => now(),
]);
$bucket = MailQuotaBucket::where('period_key', $key)->lockForUpdate()->firstOrFail();
$bucket->used = max($bucket->used, (int) $usage[$period]['used']);
$bucket->limit = $limit;
$bucket->synced_at = now();
$bucket->save();
$buckets[] = $bucket;
}
foreach ($buckets as $bucket) {
if ($bucket->used + $bucket->reserved >= $bucket->limit) {
return false;
}
}
foreach ($buckets as $bucket) {
$bucket->increment('reserved');
}
$message->reservation = array_map(fn ($bucket) => $bucket->id, $buckets);
$message->save();
return true;
}, 3);
}
// Call inside the same transaction that records the message result.
public function settle(MailMessage $message, bool $accepted): void
{
foreach ($message->reservation ?? [] as $id) {
$bucket = MailQuotaBucket::whereKey($id)->lockForUpdate()->firstOrFail();
$bucket->reserved = max(0, $bucket->reserved - 1);
if ($accepted) {
$bucket->used++;
}
$bucket->save();
}
$message->reservation = null;
$message->save();
}
public function exhausted(string $period): void
{
Cache::forget('mailflow:resend:usage');
MailQuotaBucket::where('period', $period)->where('resets_at', '>', now())
->update(['used' => DB::raw('`limit`')]);
}
}