Initial Ladill Hosting app with Gitea deploy pipeline.
Deploy Ladill Hosting / deploy (push) Failing after 17s
Deploy Ladill Hosting / deploy (push) Failing after 17s
Shared web hosting extracted from the platform monolith, with CI deploy to /var/www/ladill-hosting matching Bird/Domains/Email. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,642 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Hosting;
|
||||
|
||||
use App\Models\CustomerHostingOrder;
|
||||
use App\Models\HostingAccount;
|
||||
use App\Models\HostingAccountAlert;
|
||||
use App\Models\HostingProduct;
|
||||
use App\Notifications\HostingResourceWarningNotification;
|
||||
use App\Services\Hosting\Providers\SharedNodeProvider;
|
||||
|
||||
class HostingResourcePolicyService
|
||||
{
|
||||
public const DISK_WARNING_THRESHOLD = 90;
|
||||
public const INODE_WARNING_THRESHOLD = 90;
|
||||
private const AUTOMATIC_SUSPENSION_GRACE_HOURS = 24;
|
||||
private const AUTOMATIC_SUSPENSION_MIN_BREACH_STREAK = 12;
|
||||
|
||||
public function __construct(
|
||||
private SharedNodeProvider $sharedNodeProvider
|
||||
) {
|
||||
}
|
||||
|
||||
public function defaultLimitsForProduct(?HostingProduct $product): array
|
||||
{
|
||||
$defaults = match ($product?->type) {
|
||||
HostingProduct::TYPE_WORDPRESS => [
|
||||
'cpu_limit_percent' => 113,
|
||||
'memory_limit_mb' => 1152,
|
||||
'process_limit' => 23,
|
||||
'io_limit_mb' => 12,
|
||||
'inode_limit' => 450000,
|
||||
],
|
||||
HostingProduct::TYPE_MULTI_DOMAIN => [
|
||||
'cpu_limit_percent' => 150,
|
||||
'memory_limit_mb' => 1536,
|
||||
'process_limit' => 30,
|
||||
'io_limit_mb' => 15,
|
||||
'inode_limit' => 525000,
|
||||
],
|
||||
default => [
|
||||
'cpu_limit_percent' => 75,
|
||||
'memory_limit_mb' => 768,
|
||||
'process_limit' => 15,
|
||||
'io_limit_mb' => 8,
|
||||
'inode_limit' => 375000,
|
||||
],
|
||||
};
|
||||
|
||||
$defaults = array_merge($defaults, $this->productSpecificLimits($product));
|
||||
|
||||
$overrides = (array) data_get($product?->features, 'resource_limits', []);
|
||||
|
||||
return array_merge($defaults, array_filter($overrides, fn ($value) => $value !== null));
|
||||
}
|
||||
|
||||
private function productSpecificLimits(?HostingProduct $product): array
|
||||
{
|
||||
return match ($product?->slug) {
|
||||
'starter-plan' => [
|
||||
'inode_limit' => 225000,
|
||||
],
|
||||
'basic-plan' => [
|
||||
'inode_limit' => 450000,
|
||||
],
|
||||
'plus-plan' => [
|
||||
'inode_limit' => 450000,
|
||||
],
|
||||
'growth-plan' => [
|
||||
'inode_limit' => 750000,
|
||||
],
|
||||
'pro-plan' => [
|
||||
'inode_limit' => 1200000,
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
public function applyDefaultLimits(HostingAccount $account, bool $persist = true): array
|
||||
{
|
||||
$defaults = $this->defaultLimitsForProduct($account->product);
|
||||
$resourceLimits = (array) ($account->resource_limits ?? []);
|
||||
|
||||
$updates = [];
|
||||
foreach ($defaults as $column => $value) {
|
||||
if ($account->{$column} === null) {
|
||||
$updates[$column] = $value;
|
||||
}
|
||||
|
||||
$resourceLimits[$column] = $account->{$column} ?? $value;
|
||||
}
|
||||
|
||||
if (! isset($resourceLimits['disk_gb'])) {
|
||||
$resourceLimits['disk_gb'] = $account->requestedDiskGb();
|
||||
}
|
||||
|
||||
if ($persist) {
|
||||
$updates['resource_limits'] = $resourceLimits;
|
||||
$updates['resource_status'] = $account->resource_status ?: HostingAccount::RESOURCE_STATUS_ACTIVE;
|
||||
|
||||
if ($updates !== []) {
|
||||
$account->update($updates);
|
||||
$account->refresh();
|
||||
}
|
||||
} else {
|
||||
$account->forceFill(array_merge($updates, [
|
||||
'resource_limits' => $resourceLimits,
|
||||
'resource_status' => $account->resource_status ?: HostingAccount::RESOURCE_STATUS_ACTIVE,
|
||||
]));
|
||||
}
|
||||
|
||||
return $resourceLimits;
|
||||
}
|
||||
|
||||
public function summarize(HostingAccount $account): array
|
||||
{
|
||||
$this->applyDefaultLimits($account);
|
||||
|
||||
return [
|
||||
'resource_status' => $account->resource_status ?: HostingAccount::RESOURCE_STATUS_ACTIVE,
|
||||
'uploads_restricted' => $account->uploadsAreRestricted(),
|
||||
'disk_usage_percent' => $account->diskUsagePercent(),
|
||||
'inode_usage_percent' => $account->inodeUsagePercent(),
|
||||
'cpu_usage_percent' => (float) ($account->cpu_usage_percent ?? 0),
|
||||
'memory_used_mb' => (int) ($account->memory_used_mb ?? 0),
|
||||
'process_count' => (int) ($account->process_count ?? 0),
|
||||
'io_usage_mb' => $account->io_usage_mb !== null ? (float) $account->io_usage_mb : null,
|
||||
'cpu_limit_percent' => (int) ($account->cpu_limit_percent ?? 0),
|
||||
'memory_limit_mb' => (int) ($account->memory_limit_mb ?? 0),
|
||||
'process_limit' => (int) ($account->process_limit ?? 0),
|
||||
'io_limit_mb' => (int) ($account->io_limit_mb ?? 0),
|
||||
'inode_limit' => (int) ($account->inode_limit ?? 0),
|
||||
'warning_count' => (int) ($account->warning_count ?? 0),
|
||||
'is_flagged' => (bool) ($account->is_flagged ?? false),
|
||||
'last_usage_sync_at' => $account->last_usage_sync_at,
|
||||
'last_warning_at' => $account->last_warning_at,
|
||||
'throttled_at' => $account->throttled_at,
|
||||
];
|
||||
}
|
||||
|
||||
public function process(HostingAccount $account): array
|
||||
{
|
||||
$account->loadMissing(['product', 'user', 'latestOrder']);
|
||||
$this->applyDefaultLimits($account);
|
||||
|
||||
$evaluation = $this->evaluate($account);
|
||||
$breaches = $evaluation['breaches'];
|
||||
$warnings = $evaluation['warnings'];
|
||||
$warningMessages = $evaluation['warning_messages'];
|
||||
$primaryReason = $evaluation['primary_reason'];
|
||||
|
||||
$this->updateBreachCounters($account, $breaches);
|
||||
|
||||
$account->forceFill([
|
||||
'uploads_restricted' => array_key_exists('inode', $breaches) || $account->inode_count >= (int) $account->inode_limit,
|
||||
'is_flagged' => $warnings !== [] || $breaches !== [],
|
||||
'metadata' => $account->metadata, // Save CPU history from evaluation
|
||||
]);
|
||||
|
||||
if ($warnings !== [] || $breaches !== []) {
|
||||
$account->forceFill([
|
||||
'warning_count' => (int) $account->warning_count + 1,
|
||||
'last_warning_at' => now(),
|
||||
'last_resource_breach_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
$account->save();
|
||||
$account->refresh();
|
||||
|
||||
foreach ($warningMessages as $type => $message) {
|
||||
$severity = array_key_exists($type, $breaches) ? 'critical' : 'warning';
|
||||
$alertType = array_key_exists($type, $breaches)
|
||||
? HostingAccountAlert::TYPE_LIMIT_EXCEEDED
|
||||
: HostingAccountAlert::TYPE_WARNING;
|
||||
|
||||
$this->recordAlert($account, $alertType, $severity, $message, $evaluation['usage']);
|
||||
}
|
||||
|
||||
if ($breaches !== []) {
|
||||
if ($account->isThrottled()) {
|
||||
if ($this->shouldAutomaticallySuspendForSustainedBreach($account, $breaches)) {
|
||||
$this->suspendAccount($account, $primaryReason, false);
|
||||
}
|
||||
} else {
|
||||
$this->throttleAccount($account, $primaryReason, false);
|
||||
}
|
||||
} elseif ($warnings !== []) {
|
||||
$this->notifyAccount($account, 'Hosting usage warning', implode(' ', array_values($warningMessages)), true);
|
||||
} else {
|
||||
$this->clearWarningState($account);
|
||||
}
|
||||
|
||||
if ($breaches === []) {
|
||||
if ($this->shouldAutomaticallyUnsuspend($account)) {
|
||||
$this->unsuspendAccount($account, 'Usage returned within allowed thresholds.');
|
||||
} elseif ($account->isThrottled() && $account->status === HostingAccount::STATUS_ACTIVE) {
|
||||
$this->restoreAccount($account, 'Usage returned within allowed thresholds.', false);
|
||||
}
|
||||
}
|
||||
|
||||
return $evaluation;
|
||||
}
|
||||
|
||||
public function throttleAccount(HostingAccount $account, string $reason, bool $manual = true): void
|
||||
{
|
||||
$account->loadMissing(['user', 'latestOrder']);
|
||||
|
||||
if ($account->status === HostingAccount::STATUS_SUSPENDED) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sharedNodeProvider->applyAccountResourceProfile($account, true);
|
||||
|
||||
$metadata = array_merge((array) ($account->metadata ?? []), [
|
||||
'throttle_reason' => $reason,
|
||||
'throttle_origin' => $manual ? 'manual' : 'automatic',
|
||||
'throttled_at' => now()->toIso8601String(),
|
||||
]);
|
||||
|
||||
$account->update([
|
||||
'resource_status' => HostingAccount::RESOURCE_STATUS_THROTTLED,
|
||||
'throttled_at' => now(),
|
||||
'metadata' => $metadata,
|
||||
'uploads_restricted' => $account->uploadsAreRestricted(),
|
||||
]);
|
||||
|
||||
$this->syncLatestOrderState($account, CustomerHostingOrder::STATUS_ACTIVE, $reason);
|
||||
$this->recordAlert($account, HostingAccountAlert::TYPE_THROTTLED, 'critical', $reason, $this->usageSnapshot($account));
|
||||
$this->notifyAccount($account, 'Hosting account throttled', $reason, false);
|
||||
}
|
||||
|
||||
public function restoreAccount(HostingAccount $account, ?string $reason = null, bool $manual = true): void
|
||||
{
|
||||
$account->loadMissing(['user', 'latestOrder']);
|
||||
|
||||
if ($account->status === HostingAccount::STATUS_SUSPENDED) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sharedNodeProvider->applyAccountResourceProfile($account, false);
|
||||
|
||||
$metadata = (array) ($account->metadata ?? []);
|
||||
$metadata['restored_at'] = now()->toIso8601String();
|
||||
$metadata['restore_origin'] = $manual ? 'manual' : 'automatic';
|
||||
// Clear CPU history on restore
|
||||
unset($metadata['cpu_history'], $metadata['cpu_averages']);
|
||||
|
||||
$account->update([
|
||||
'resource_status' => HostingAccount::RESOURCE_STATUS_ACTIVE,
|
||||
'uploads_restricted' => false,
|
||||
'is_flagged' => false,
|
||||
'cpu_breach_streak' => 0,
|
||||
'memory_breach_streak' => 0,
|
||||
'process_breach_streak' => 0,
|
||||
'io_breach_streak' => 0,
|
||||
'inode_breach_streak' => 0,
|
||||
'throttled_at' => null,
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
|
||||
$this->resolveAlerts($account);
|
||||
|
||||
if ($reason) {
|
||||
$this->notifyAccount($account, 'Hosting account restored', $reason, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function suspendAccount(HostingAccount $account, string $reason, bool $manual = true): void
|
||||
{
|
||||
$account->loadMissing(['sites', 'user', 'latestOrder']);
|
||||
|
||||
if ($account->status === HostingAccount::STATUS_SUSPENDED) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sharedNodeProvider->suspendAccount($account, $reason);
|
||||
|
||||
$metadata = array_merge((array) ($account->metadata ?? []), [
|
||||
'suspension_reason' => $reason,
|
||||
'suspension_origin' => $manual ? 'manual' : 'automatic',
|
||||
'suspended_at' => now()->toIso8601String(),
|
||||
]);
|
||||
|
||||
$account->update([
|
||||
'status' => HostingAccount::STATUS_SUSPENDED,
|
||||
'resource_status' => HostingAccount::RESOURCE_STATUS_SUSPENDED,
|
||||
'suspension_reason' => $reason,
|
||||
'suspended_at' => now(),
|
||||
'is_flagged' => true,
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
|
||||
$this->syncLatestOrderState($account, CustomerHostingOrder::STATUS_SUSPENDED, $reason);
|
||||
$this->recordAlert($account, HostingAccountAlert::TYPE_SUSPENDED, 'critical', $reason, $this->usageSnapshot($account));
|
||||
}
|
||||
|
||||
public function unsuspendAccount(HostingAccount $account, ?string $reason = null): void
|
||||
{
|
||||
$account->loadMissing(['sites', 'user', 'latestOrder']);
|
||||
|
||||
if ($account->status !== HostingAccount::STATUS_SUSPENDED) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sharedNodeProvider->unsuspendAccount($account);
|
||||
$this->sharedNodeProvider->applyAccountResourceProfile($account, false);
|
||||
|
||||
$metadata = (array) ($account->metadata ?? []);
|
||||
$metadata['unsuspended_at'] = now()->toIso8601String();
|
||||
|
||||
$account->update([
|
||||
'status' => HostingAccount::STATUS_ACTIVE,
|
||||
'resource_status' => HostingAccount::RESOURCE_STATUS_ACTIVE,
|
||||
'suspension_reason' => null,
|
||||
'suspended_at' => null,
|
||||
'uploads_restricted' => false,
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
|
||||
$this->syncLatestOrderState($account, CustomerHostingOrder::STATUS_ACTIVE, $reason);
|
||||
$this->resolveAlerts($account);
|
||||
}
|
||||
|
||||
private function evaluate(HostingAccount $account): array
|
||||
{
|
||||
$usage = $this->usageSnapshot($account);
|
||||
$warnings = [];
|
||||
$breaches = [];
|
||||
$messages = [];
|
||||
|
||||
if ($usage['disk_usage_percent'] >= self::DISK_WARNING_THRESHOLD) {
|
||||
$warnings['disk'] = true;
|
||||
$messages['disk'] = sprintf(
|
||||
'Disk usage is at %.1f%% of the allocated quota.',
|
||||
$usage['disk_usage_percent']
|
||||
);
|
||||
}
|
||||
|
||||
if ($usage['inode_usage_percent'] >= self::INODE_WARNING_THRESHOLD) {
|
||||
$warnings['inode'] = true;
|
||||
$messages['inode'] = sprintf(
|
||||
'Inode usage is at %.1f%% of the allowed limit.',
|
||||
$usage['inode_usage_percent']
|
||||
);
|
||||
}
|
||||
|
||||
// CPU: Use time-based sustained usage, not instant spikes
|
||||
if ($account->cpu_limit_percent) {
|
||||
$cpuEvaluation = $this->evaluateCpuUsage($account, $usage['cpu_usage_percent'], $account->cpu_limit_percent);
|
||||
if ($cpuEvaluation['is_breach']) {
|
||||
$breaches['cpu'] = true;
|
||||
$messages['cpu'] = $cpuEvaluation['message'];
|
||||
} elseif ($cpuEvaluation['is_warning']) {
|
||||
$warnings['cpu'] = true;
|
||||
$messages['cpu'] = $cpuEvaluation['message'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($account->memory_limit_mb && $usage['memory_used_mb'] > $account->memory_limit_mb) {
|
||||
$breaches['memory'] = true;
|
||||
$messages['memory'] = sprintf(
|
||||
'Memory usage reached %d MB against a %d MB limit.',
|
||||
$usage['memory_used_mb'],
|
||||
$account->memory_limit_mb
|
||||
);
|
||||
}
|
||||
|
||||
if ($account->process_limit && $usage['process_count'] > $account->process_limit) {
|
||||
$breaches['process'] = true;
|
||||
$messages['process'] = sprintf(
|
||||
'Process count reached %d against a %d process limit.',
|
||||
$usage['process_count'],
|
||||
$account->process_limit
|
||||
);
|
||||
}
|
||||
|
||||
if ($account->io_limit_mb && $usage['io_usage_mb'] !== null && $usage['io_usage_mb'] > $account->io_limit_mb) {
|
||||
$breaches['io'] = true;
|
||||
$messages['io'] = sprintf(
|
||||
'I/O usage reached %.1f MB against a %d MB limit.',
|
||||
$usage['io_usage_mb'],
|
||||
$account->io_limit_mb
|
||||
);
|
||||
}
|
||||
|
||||
if ($account->inode_limit && $usage['inode_count'] > $account->inode_limit) {
|
||||
$breaches['inode'] = true;
|
||||
$messages['inode_limit'] = sprintf(
|
||||
'Inode count reached %d against a %d inode limit.',
|
||||
$usage['inode_count'],
|
||||
$account->inode_limit
|
||||
);
|
||||
}
|
||||
|
||||
$primaryReason = $messages[array_key_first($breaches)] ?? implode(' ', array_values($messages)) ?: 'Resource policy triggered.';
|
||||
|
||||
return [
|
||||
'usage' => $usage,
|
||||
'warnings' => $warnings,
|
||||
'breaches' => $breaches,
|
||||
'warning_messages' => $messages,
|
||||
'primary_reason' => $primaryReason,
|
||||
];
|
||||
}
|
||||
|
||||
private function updateBreachCounters(HostingAccount $account, array $breaches): void
|
||||
{
|
||||
foreach ([
|
||||
'cpu',
|
||||
'memory',
|
||||
'process',
|
||||
'io',
|
||||
'inode',
|
||||
] as $metric) {
|
||||
$column = "{$metric}_breach_streak";
|
||||
$account->{$column} = array_key_exists($metric, $breaches)
|
||||
? ((int) $account->{$column}) + 1
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
|
||||
private function evaluateCpuUsage(HostingAccount $account, float $currentCpu, int $limit): array
|
||||
{
|
||||
$metadata = (array) ($account->metadata ?? []);
|
||||
$cpuHistory = (array) ($metadata['cpu_history'] ?? []);
|
||||
|
||||
// Add current reading with timestamp
|
||||
$now = now()->timestamp;
|
||||
$cpuHistory[] = ['ts' => $now, 'cpu' => $currentCpu];
|
||||
|
||||
// Keep only last 30 minutes of data (sync runs every 5 min = ~6 readings)
|
||||
$cutoff = $now - 1800; // 30 minutes
|
||||
$cpuHistory = array_filter($cpuHistory, fn($r) => $r['ts'] > $cutoff);
|
||||
$cpuHistory = array_values($cpuHistory);
|
||||
|
||||
// Calculate averages over time windows
|
||||
$windows = [
|
||||
'1m' => 60, // 1 minute
|
||||
'5m' => 300, // 5 minutes
|
||||
'15m' => 900, // 15 minutes
|
||||
];
|
||||
|
||||
$averages = [];
|
||||
foreach ($windows as $name => $seconds) {
|
||||
$windowCutoff = $now - $seconds;
|
||||
$windowReadings = array_filter($cpuHistory, fn($r) => $r['ts'] > $windowCutoff);
|
||||
$count = count($windowReadings);
|
||||
|
||||
if ($count > 0) {
|
||||
$sum = array_sum(array_column($windowReadings, 'cpu'));
|
||||
$averages[$name] = $sum / $count;
|
||||
} else {
|
||||
$averages[$name] = $currentCpu;
|
||||
}
|
||||
}
|
||||
|
||||
// Save updated history
|
||||
$metadata['cpu_history'] = $cpuHistory;
|
||||
$metadata['cpu_averages'] = $averages;
|
||||
$account->metadata = $metadata;
|
||||
|
||||
// Evaluation rules:
|
||||
// - Warning threshold: 80% of limit, sustained for 5 minutes
|
||||
// - Breach threshold: 100% of limit, sustained for 10 minutes
|
||||
// - Extreme breach: 120% of limit, sustained for 5 minutes
|
||||
|
||||
$warningThreshold = $limit * 0.8;
|
||||
$breachThreshold = $limit;
|
||||
$extremeThreshold = $limit * 1.2;
|
||||
|
||||
$isWarning = false;
|
||||
$isBreach = false;
|
||||
$message = '';
|
||||
|
||||
// Check for extreme breach (immediate action)
|
||||
if ($averages['5m'] >= $extremeThreshold) {
|
||||
$isBreach = true;
|
||||
$message = sprintf(
|
||||
'CPU usage critically high: %.1f%% average over 5 minutes (limit: %d%%).',
|
||||
$averages['5m'],
|
||||
$limit
|
||||
);
|
||||
}
|
||||
// Check for sustained breach (10+ minutes at or above limit)
|
||||
elseif ($averages['15m'] >= $breachThreshold) {
|
||||
$isBreach = true;
|
||||
$message = sprintf(
|
||||
'CPU usage sustained above limit: %.1f%% average over 15 minutes (limit: %d%%).',
|
||||
$averages['15m'],
|
||||
$limit
|
||||
);
|
||||
}
|
||||
// Check for sustained warning (5+ minutes at 80%+ of limit)
|
||||
elseif ($averages['5m'] >= $warningThreshold) {
|
||||
$isWarning = true;
|
||||
$message = sprintf(
|
||||
'CPU usage elevated: %.1f%% average over 5 minutes (limit: %d%%). Monitor closely.',
|
||||
$averages['5m'],
|
||||
$limit
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'is_breach' => $isBreach,
|
||||
'is_warning' => $isWarning,
|
||||
'message' => $message,
|
||||
'averages' => $averages,
|
||||
];
|
||||
}
|
||||
|
||||
private function usageSnapshot(HostingAccount $account): array
|
||||
{
|
||||
return [
|
||||
'disk_used_bytes' => (int) $account->disk_used_bytes,
|
||||
'disk_usage_percent' => $account->diskUsagePercent(),
|
||||
'inode_count' => (int) $account->inode_count,
|
||||
'inode_usage_percent' => $account->inodeUsagePercent(),
|
||||
'cpu_usage_percent' => (float) ($account->cpu_usage_percent ?? 0),
|
||||
'memory_used_mb' => (int) ($account->memory_used_mb ?? 0),
|
||||
'process_count' => (int) ($account->process_count ?? 0),
|
||||
'io_usage_mb' => $account->io_usage_mb !== null ? (float) $account->io_usage_mb : null,
|
||||
];
|
||||
}
|
||||
|
||||
private function clearWarningState(HostingAccount $account): void
|
||||
{
|
||||
$account->update([
|
||||
'is_flagged' => false,
|
||||
'uploads_restricted' => false,
|
||||
'cpu_breach_streak' => 0,
|
||||
'memory_breach_streak' => 0,
|
||||
'process_breach_streak' => 0,
|
||||
'io_breach_streak' => 0,
|
||||
'inode_breach_streak' => 0,
|
||||
]);
|
||||
|
||||
$this->resolveAlerts($account);
|
||||
}
|
||||
|
||||
private function shouldAutomaticallyUnsuspend(HostingAccount $account): bool
|
||||
{
|
||||
return $account->status === HostingAccount::STATUS_SUSPENDED
|
||||
&& $account->resource_status === HostingAccount::RESOURCE_STATUS_SUSPENDED
|
||||
&& data_get($account->metadata, 'suspension_origin') === 'automatic';
|
||||
}
|
||||
|
||||
private function shouldAutomaticallySuspendForSustainedBreach(HostingAccount $account, array $breaches): bool
|
||||
{
|
||||
if (! $account->throttled_at || $account->throttled_at->gt(now()->subHours(self::AUTOMATIC_SUSPENSION_GRACE_HOURS))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (array_keys($breaches) as $metric) {
|
||||
$column = "{$metric}_breach_streak";
|
||||
|
||||
if ((int) ($account->{$column} ?? 0) >= self::AUTOMATIC_SUSPENSION_MIN_BREACH_STREAK) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function recordAlert(HostingAccount $account, string $alertType, string $severity, string $message, array $usage): void
|
||||
{
|
||||
$existing = $account->alerts()
|
||||
->where('alert_type', $alertType)
|
||||
->where('is_resolved', false)
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$existing->update([
|
||||
'severity' => $severity,
|
||||
'message' => $message,
|
||||
'resource_usage' => $usage,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$account->alerts()->create([
|
||||
'alert_type' => $alertType,
|
||||
'severity' => $severity,
|
||||
'message' => $message,
|
||||
'resource_usage' => $usage,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveAlerts(HostingAccount $account): void
|
||||
{
|
||||
$account->alerts()
|
||||
->unresolved()
|
||||
->update([
|
||||
'is_resolved' => true,
|
||||
'resolved_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function notifyAccount(HostingAccount $account, string $subject, string $message, bool $respectCooldown): void
|
||||
{
|
||||
if (! $account->user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
$respectCooldown
|
||||
&& (int) $account->warning_count > 1
|
||||
&& $account->last_warning_at
|
||||
&& $account->last_warning_at->gt(now()->subHours(6))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$account->user->notify(new HostingResourceWarningNotification($account, $subject, $message));
|
||||
}
|
||||
|
||||
private function syncLatestOrderState(HostingAccount $account, string $status, ?string $reason = null): void
|
||||
{
|
||||
$order = $account->latestOrder;
|
||||
|
||||
if (! $order) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($status === CustomerHostingOrder::STATUS_SUSPENDED) {
|
||||
$order->suspend($reason);
|
||||
return;
|
||||
}
|
||||
|
||||
$meta = array_merge((array) ($order->meta ?? []), [
|
||||
'resource_policy_note' => $reason,
|
||||
]);
|
||||
|
||||
$order->update([
|
||||
'status' => $status,
|
||||
'suspended_at' => $status === CustomerHostingOrder::STATUS_SUSPENDED ? now() : null,
|
||||
'meta' => $meta,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user