option('tenant'))); $olderThanMinutes = max(0, (int) $this->option('older-than')); $dryRun = (bool) $this->option('dry-run'); $query = OperationRun::query() ->where('type', 'backup_schedule_run') ->whereIn('status', ['queued', 'running']); if ($olderThanMinutes > 0) { $query->where('created_at', '<', now()->subMinutes($olderThanMinutes)); } if ($tenantIdentifiers !== []) { $tenantIds = $this->resolveTenantIds($tenantIdentifiers); if ($tenantIds === []) { $this->info('No tenants matched the provided identifiers.'); return self::SUCCESS; } $query->whereIn('tenant_id', $tenantIds); } $reconciled = 0; $skipped = 0; $failed = 0; foreach ($query->cursor() as $operationRun) { $backupScheduleId = data_get($operationRun->context, 'backup_schedule_id'); if (! is_numeric($backupScheduleId)) { if (! $dryRun) { $operationRunService->updateRun( $operationRun, status: 'completed', outcome: OperationRunOutcome::Failed->value, failures: [ [ 'code' => 'backup_schedule.missing_context', 'message' => 'Backup schedule context is missing from this operation run.', ], ], ); } $failed++; continue; } $schedule = BackupSchedule::query() ->whereKey((int) $backupScheduleId) ->where('tenant_id', (int) $operationRun->tenant_id) ->first(); if (! $schedule instanceof BackupSchedule) { if (! $dryRun) { $operationRunService->updateRun( $operationRun, status: 'completed', outcome: OperationRunOutcome::Failed->value, failures: [ [ 'code' => 'backup_schedule.not_found', 'message' => 'Backup schedule not found for this operation run.', ], ], ); } $failed++; continue; } if ($operationRun->status === 'queued' && $operationRunService->isStaleQueuedRun($operationRun, max(1, $olderThanMinutes))) { if (! $dryRun) { $operationRunService->failStaleQueuedRun($operationRun, 'Backup schedule run was queued but never started.'); } $reconciled++; continue; } if ($operationRun->status === 'running') { if (! $dryRun) { $operationRunService->updateRun( $operationRun, status: 'completed', outcome: OperationRunOutcome::Failed->value, failures: [ [ 'code' => 'backup_schedule.stalled', 'message' => 'Backup schedule run exceeded reconciliation timeout and was marked failed.', ], ], ); } $reconciled++; continue; } $skipped++; } $this->info(sprintf( 'Reconciled %d run(s), skipped %d, failed %d.', $reconciled, $skipped, $failed, )); if ($dryRun) { $this->comment('Dry-run: no changes written.'); } return self::SUCCESS; } /** * @param array $tenantIdentifiers * @return array */ private function resolveTenantIds(array $tenantIdentifiers): array { $tenantIds = []; foreach ($tenantIdentifiers as $identifier) { $tenant = Tenant::query() ->forTenant($identifier) ->first(); if ($tenant) { $tenantIds[] = (int) $tenant->getKey(); } } return array_values(array_unique($tenantIds)); } }