62 lines
3.2 KiB
PHP
62 lines
3.2 KiB
PHP
<?php
|
|
|
|
namespace App\Serp\Operations;
|
|
|
|
use App\Models\SerpBudgetEntry;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
final class BudgetManager
|
|
{
|
|
public function __construct(private KillSwitch $kill, private OperationAuditLogger $audit, private RampUpPolicy $ramp) {}
|
|
|
|
public function reserve(string $key, string $mode, ?int $checkId = null): SerpBudgetEntry
|
|
{
|
|
return Cache::lock('serp-budget-global', 10)->block(5, function () use ($key, $mode, $checkId) {
|
|
$result = DB::transaction(function () use ($key, $mode, $checkId) {
|
|
if ($e = SerpBudgetEntry::where('reservation_key', $key)->lockForUpdate()->first()) {
|
|
return $e;
|
|
}
|
|
|
|
$this->ramp->assertAllowsNewPaid();
|
|
$estimate = Money::micros((string) config('serp.estimated_costs.'.$mode));
|
|
$day = SerpBudgetEntry::where('reserved_at', '>=', now('UTC')->startOfDay())->sum(DB::raw('CASE WHEN actual_micros IS NULL THEN reserved_micros ELSE actual_micros END'));
|
|
$month = SerpBudgetEntry::where('reserved_at', '>=', now('UTC')->startOfMonth())->sum(DB::raw('CASE WHEN actual_micros IS NULL THEN reserved_micros ELSE actual_micros END'));
|
|
if ($day + $estimate > Money::micros((string) config('serp.daily_budget_usd')) || $month + $estimate > Money::micros((string) config('serp.monthly_budget_usd'))) {
|
|
return false;
|
|
}
|
|
|
|
return SerpBudgetEntry::create(['reservation_key' => $key, 'serp_check_id' => $checkId, 'mode' => $mode, 'reserved_micros' => $estimate, 'currency' => 'USD', 'reserved_at' => now('UTC')]);
|
|
});
|
|
if ($result === false) {
|
|
$this->kill->activate('budget_exceeded');
|
|
$this->audit->record('budget_blocked', ['cost_micros' => Money::micros((string) config('serp.estimated_costs.'.$mode))]);
|
|
throw new OperationBlockedException('budget_blocked');
|
|
}
|
|
|
|
$this->audit->record('budget_reserved', ['mode' => $mode, 'cost_micros' => $result->reserved_micros]);
|
|
|
|
return $result;
|
|
});
|
|
}
|
|
|
|
public function reconcile(string $key, string|int|float|null $actual, ?string $taskId = null): void
|
|
{
|
|
DB::transaction(function () use ($key, $actual, $taskId) {
|
|
$e = SerpBudgetEntry::where('reservation_key', $key)->lockForUpdate()->first();
|
|
if (! $e || $e->reconciled_at) {
|
|
return;
|
|
}$e->forceFill(['provider_task_id' => $taskId, 'actual_micros' => Money::micros($actual), 'reconciled_at' => now('UTC')])->save();
|
|
$this->audit->record('budget_reconciled', ['cost_micros' => $e->actual_micros]);
|
|
});
|
|
}
|
|
|
|
public function usage(): array
|
|
{
|
|
$daily = (int) SerpBudgetEntry::where('reserved_at', '>=', now('UTC')->startOfDay())->sum(DB::raw('CASE WHEN actual_micros IS NULL THEN reserved_micros ELSE actual_micros END'));
|
|
$monthly = (int) SerpBudgetEntry::where('reserved_at', '>=', now('UTC')->startOfMonth())->sum(DB::raw('CASE WHEN actual_micros IS NULL THEN reserved_micros ELSE actual_micros END'));
|
|
|
|
return ['daily_micros' => $daily, 'monthly_micros' => $monthly];
|
|
}
|
|
}
|