find($this->bulkRunId); if (! $run || $run->status !== 'pending') { return; } $service->start($run); $itemCount = 0; $succeeded = 0; $failed = 0; $skipped = 0; $skipReasons = []; $chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10)); $totalItems = $run->total_items ?: count($run->item_ids ?? []); $failureThreshold = (int) floor($totalItems / 2); foreach (($run->item_ids ?? []) as $restoreRunId) { $itemCount++; try { /** @var RestoreRun|null $restoreRun */ $restoreRun = RestoreRun::withTrashed() ->where('tenant_id', $run->tenant_id) ->whereKey($restoreRunId) ->first(); if (! $restoreRun) { $service->recordFailure($run, (string) $restoreRunId, 'Restore run not found'); $failed++; if ($failed > $failureThreshold) { $service->abort($run, 'Circuit breaker: more than 50% of items failed.'); if ($run->user) { Notification::make() ->title('Bulk Restore Aborted') ->body('Circuit breaker triggered: too many failures (>50%).') ->icon('heroicon-o-exclamation-triangle') ->danger() ->sendToDatabase($run->user) ->send(); } return; } continue; } if (! $restoreRun->trashed()) { $service->recordSkippedWithReason($run, (string) $restoreRun->id, 'Not archived'); $skipped++; $skipReasons['Not archived'] = ($skipReasons['Not archived'] ?? 0) + 1; continue; } $restoreRun->restore(); $service->recordSuccess($run); $succeeded++; } catch (Throwable $e) { $service->recordFailure($run, (string) $restoreRunId, $e->getMessage()); $failed++; if ($failed > $failureThreshold) { $service->abort($run, 'Circuit breaker: more than 50% of items failed.'); if ($run->user) { Notification::make() ->title('Bulk Restore Aborted') ->body('Circuit breaker triggered: too many failures (>50%).') ->icon('heroicon-o-exclamation-triangle') ->danger() ->sendToDatabase($run->user) ->send(); } return; } } if ($itemCount % $chunkSize === 0) { $run->refresh(); } } $service->complete($run); if (! $run->user) { return; } $message = "Restored {$succeeded} restore runs"; if ($skipped > 0) { $message .= " ({$skipped} skipped)"; } if ($failed > 0) { $message .= " ({$failed} failed)"; } if (! empty($skipReasons)) { $summary = collect($skipReasons) ->sortDesc() ->map(fn (int $count, string $reason) => "{$reason} ({$count})") ->take(3) ->implode(', '); if ($summary !== '') { $message .= " Skip reasons: {$summary}."; } } $message .= '.'; Notification::make() ->title('Bulk Restore Completed') ->body($message) ->icon('heroicon-o-check-circle') ->success() ->sendToDatabase($run->user) ->send(); } }