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); } }