217 lines
7.0 KiB
PHP
217 lines
7.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Jobs\Alerts;
|
|
|
|
use App\Models\AlertDelivery;
|
|
use App\Models\OperationRun;
|
|
use App\Models\Workspace;
|
|
use App\Services\Alerts\AlertSender;
|
|
use App\Services\OperationRunService;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Throwable;
|
|
|
|
class DeliverAlertsJob implements ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
public function __construct(
|
|
public int $workspaceId,
|
|
public ?int $operationRunId = null,
|
|
) {}
|
|
|
|
public function handle(AlertSender $alertSender, OperationRunService $operationRuns): void
|
|
{
|
|
$workspace = Workspace::query()->whereKey($this->workspaceId)->first();
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
return;
|
|
}
|
|
|
|
$operationRun = $this->resolveOperationRun($workspace, $operationRuns);
|
|
|
|
if (! $operationRun instanceof OperationRun) {
|
|
return;
|
|
}
|
|
|
|
if ($operationRun->status === OperationRunStatus::Completed->value) {
|
|
return;
|
|
}
|
|
|
|
$operationRuns->updateRun(
|
|
$operationRun,
|
|
status: OperationRunStatus::Running->value,
|
|
outcome: OperationRunOutcome::Pending->value,
|
|
);
|
|
|
|
$now = CarbonImmutable::now('UTC');
|
|
$batchSize = max(1, (int) config('tenantpilot.alerts.deliver_batch_size', 200));
|
|
$maxAttempts = max(1, (int) config('tenantpilot.alerts.delivery_max_attempts', 3));
|
|
|
|
$deliveries = AlertDelivery::query()
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->whereIn('status', [
|
|
AlertDelivery::STATUS_QUEUED,
|
|
AlertDelivery::STATUS_DEFERRED,
|
|
])
|
|
->where(function ($query) use ($now): void {
|
|
$query->whereNull('send_after')
|
|
->orWhere('send_after', '<=', $now);
|
|
})
|
|
->orderBy('id')
|
|
->limit($batchSize)
|
|
->get();
|
|
|
|
$processed = 0;
|
|
$succeeded = 0;
|
|
$terminalFailures = 0;
|
|
$requeued = 0;
|
|
|
|
foreach ($deliveries as $delivery) {
|
|
if ($delivery->isTerminal()) {
|
|
continue;
|
|
}
|
|
|
|
$processed++;
|
|
|
|
try {
|
|
$alertSender->send($delivery);
|
|
|
|
$delivery->forceFill([
|
|
'attempt_count' => (int) $delivery->attempt_count + 1,
|
|
'status' => AlertDelivery::STATUS_SENT,
|
|
'send_after' => null,
|
|
'sent_at' => $now,
|
|
'last_error_code' => null,
|
|
'last_error_message' => null,
|
|
])->save();
|
|
|
|
$succeeded++;
|
|
} catch (Throwable $exception) {
|
|
$attemptCount = (int) $delivery->attempt_count + 1;
|
|
$errorCode = $this->sanitizeErrorCode($exception);
|
|
$errorMessage = $this->sanitizeErrorMessage($exception);
|
|
|
|
if ($attemptCount >= $maxAttempts) {
|
|
$delivery->forceFill([
|
|
'attempt_count' => $attemptCount,
|
|
'status' => AlertDelivery::STATUS_FAILED,
|
|
'send_after' => null,
|
|
'last_error_code' => $errorCode,
|
|
'last_error_message' => $errorMessage,
|
|
])->save();
|
|
|
|
$terminalFailures++;
|
|
|
|
continue;
|
|
}
|
|
|
|
$delivery->forceFill([
|
|
'attempt_count' => $attemptCount,
|
|
'status' => AlertDelivery::STATUS_QUEUED,
|
|
'send_after' => $now->addSeconds($this->backoffSeconds($attemptCount)),
|
|
'last_error_code' => $errorCode,
|
|
'last_error_message' => $errorMessage,
|
|
])->save();
|
|
|
|
$requeued++;
|
|
}
|
|
}
|
|
|
|
$outcome = OperationRunOutcome::Succeeded->value;
|
|
|
|
if ($terminalFailures > 0 && $succeeded === 0 && $requeued === 0) {
|
|
$outcome = OperationRunOutcome::Failed->value;
|
|
} elseif ($terminalFailures > 0 || $requeued > 0) {
|
|
$outcome = OperationRunOutcome::PartiallySucceeded->value;
|
|
}
|
|
|
|
$operationRuns->updateRun(
|
|
$operationRun,
|
|
status: OperationRunStatus::Completed->value,
|
|
outcome: $outcome,
|
|
summaryCounts: [
|
|
'total' => count($deliveries),
|
|
'processed' => $processed,
|
|
'succeeded' => $succeeded,
|
|
'failed' => $terminalFailures,
|
|
'skipped' => $requeued,
|
|
],
|
|
);
|
|
}
|
|
|
|
private function resolveOperationRun(Workspace $workspace, OperationRunService $operationRuns): ?OperationRun
|
|
{
|
|
if (is_int($this->operationRunId) && $this->operationRunId > 0) {
|
|
$operationRun = OperationRun::query()
|
|
->whereKey($this->operationRunId)
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->where('type', 'alerts.deliver')
|
|
->first();
|
|
|
|
if ($operationRun instanceof OperationRun) {
|
|
return $operationRun;
|
|
}
|
|
}
|
|
|
|
$slotKey = CarbonImmutable::now('UTC')->format('YmdHi').'Z';
|
|
|
|
return $operationRuns->ensureWorkspaceRunWithIdentity(
|
|
workspace: $workspace,
|
|
type: 'alerts.deliver',
|
|
identityInputs: [
|
|
'slot_key' => $slotKey,
|
|
],
|
|
context: [
|
|
'trigger' => 'job',
|
|
'slot_key' => $slotKey,
|
|
],
|
|
initiator: null,
|
|
);
|
|
}
|
|
|
|
private function backoffSeconds(int $attemptCount): int
|
|
{
|
|
$baseSeconds = max(1, (int) config('tenantpilot.alerts.delivery_retry_base_seconds', 60));
|
|
$maxSeconds = max($baseSeconds, (int) config('tenantpilot.alerts.delivery_retry_max_seconds', 900));
|
|
|
|
$exponent = max(0, $attemptCount - 1);
|
|
$delaySeconds = $baseSeconds * (2 ** $exponent);
|
|
|
|
return (int) min($maxSeconds, $delaySeconds);
|
|
}
|
|
|
|
private function sanitizeErrorCode(Throwable $exception): string
|
|
{
|
|
$shortName = class_basename($exception);
|
|
$shortName = trim((string) $shortName);
|
|
|
|
if ($shortName === '') {
|
|
return 'delivery_exception';
|
|
}
|
|
|
|
return strtolower(preg_replace('/[^a-z0-9]+/i', '_', $shortName) ?? 'delivery_exception');
|
|
}
|
|
|
|
private function sanitizeErrorMessage(Throwable $exception): string
|
|
{
|
|
$message = trim($exception->getMessage());
|
|
|
|
if ($message === '') {
|
|
return 'Alert delivery failed.';
|
|
}
|
|
|
|
$message = preg_replace('/https?:\/\/\S+/i', '[redacted-url]', $message) ?? $message;
|
|
|
|
return mb_substr($message, 0, 500);
|
|
}
|
|
}
|