Proje dosyaları eklendi
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
<?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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user