190 lines
7.8 KiB
PHP
190 lines
7.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Operations\Reconciliation;
|
|
|
|
use App\Models\BackupSchedule;
|
|
use App\Models\BackupSet;
|
|
use App\Models\OperationRun;
|
|
use App\Support\OperationCatalog;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\Operations\LifecycleReconciliationReason;
|
|
use Throwable;
|
|
|
|
final class BackupScheduleExecutionReconciliationAdapter implements OperationRunReconciliationAdapter
|
|
{
|
|
public function key(): string
|
|
{
|
|
return 'backup_schedule_execution';
|
|
}
|
|
|
|
public function supportedTypes(): array
|
|
{
|
|
return [OperationRunType::BackupScheduleExecute->value];
|
|
}
|
|
|
|
public function supportsType(string $type): bool
|
|
{
|
|
return OperationCatalog::canonicalCode($type) === OperationRunType::BackupScheduleExecute->value;
|
|
}
|
|
|
|
public function reconcile(OperationRun $run): ?ReconciliationResult
|
|
{
|
|
if (! $this->supportsType((string) $run->type)) {
|
|
return ReconciliationResult::unsupported(
|
|
reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value,
|
|
reasonMessage: 'This adapter only supports backup schedule execution runs.',
|
|
evidence: [
|
|
'adapter' => $this->key(),
|
|
'type' => (string) $run->type,
|
|
],
|
|
);
|
|
}
|
|
|
|
$context = is_array($run->context) ? $run->context : [];
|
|
$scheduleId = is_numeric($context['backup_schedule_id'] ?? null) ? (int) $context['backup_schedule_id'] : null;
|
|
$backupSetId = is_numeric($context['backup_set_id'] ?? null) ? (int) $context['backup_set_id'] : null;
|
|
|
|
$evidence = [
|
|
'adapter' => $this->key(),
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'workspace_id' => (int) $run->workspace_id,
|
|
'managed_environment_id' => (int) $run->managed_environment_id,
|
|
'backup_schedule_id' => $scheduleId,
|
|
'backup_set_id' => $backupSetId,
|
|
];
|
|
|
|
[$backupSet, $scopeProblem] = $this->resolveBackupSet($run, $backupSetId);
|
|
|
|
if ($scopeProblem !== null) {
|
|
return ReconciliationResult::notReconciled(
|
|
reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value,
|
|
reasonMessage: $scopeProblem,
|
|
evidence: $evidence,
|
|
);
|
|
}
|
|
|
|
if ($backupSet instanceof BackupSet) {
|
|
$failures = is_array($backupSet->metadata['failures'] ?? null) ? $backupSet->metadata['failures'] : [];
|
|
$failureCount = count($failures);
|
|
$itemCount = max(0, (int) ($backupSet->item_count ?? 0));
|
|
$summaryCounts = [
|
|
'total' => $itemCount + $failureCount,
|
|
'processed' => $itemCount + $failureCount,
|
|
'succeeded' => $itemCount,
|
|
'failed' => $failureCount,
|
|
'created' => 1,
|
|
'updated' => $itemCount,
|
|
'items' => $itemCount + $failureCount,
|
|
];
|
|
$related = [
|
|
'type' => 'backup_set',
|
|
'id' => (int) $backupSet->getKey(),
|
|
'status' => (string) $backupSet->status,
|
|
'item_count' => $itemCount,
|
|
];
|
|
|
|
if ((string) $backupSet->status === 'completed' && $failureCount === 0) {
|
|
return ReconciliationResult::reconciledSucceeded(
|
|
reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value,
|
|
reasonMessage: 'The backup schedule already produced a complete backup set before the run finished updating.',
|
|
evidence: $evidence + ['chosen_backup_set_id' => (int) $backupSet->getKey()],
|
|
related: $related,
|
|
summaryCounts: $summaryCounts,
|
|
);
|
|
}
|
|
|
|
if ($itemCount > 0 && ((string) $backupSet->status === 'partial' || $failureCount > 0)) {
|
|
return new ReconciliationResult(
|
|
decision: 'reconciled_partially_succeeded',
|
|
status: OperationRunStatus::Completed->value,
|
|
outcome: OperationRunOutcome::PartiallySucceeded->value,
|
|
reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value,
|
|
reasonMessage: 'The backup schedule produced a usable backup set, but some captures did not finish cleanly.',
|
|
evidence: $evidence + ['chosen_backup_set_id' => (int) $backupSet->getKey()],
|
|
related: $related,
|
|
summaryCounts: $summaryCounts,
|
|
failures: [[
|
|
'code' => 'backup_schedule.partial',
|
|
'reason_code' => LifecycleReconciliationReason::AdapterOutOfSync->value,
|
|
'message' => 'The backup schedule produced a usable backup set, but some captures did not finish cleanly.',
|
|
]],
|
|
safeForAutoCompletion: true,
|
|
);
|
|
}
|
|
|
|
if ((string) $backupSet->status === 'failed') {
|
|
return ReconciliationResult::failedUnrecoverable(
|
|
reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value,
|
|
reasonMessage: 'The backup schedule produced no usable backup set before the run stopped updating.',
|
|
evidence: $evidence + ['chosen_backup_set_id' => (int) $backupSet->getKey()],
|
|
related: $related,
|
|
summaryCounts: $summaryCounts,
|
|
);
|
|
}
|
|
|
|
return ReconciliationResult::notReconciled(
|
|
reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value,
|
|
reasonMessage: 'The related backup set is still being prepared and does not prove final backup truth yet.',
|
|
evidence: $evidence + ['chosen_backup_set_id' => (int) $backupSet->getKey()],
|
|
related: $related,
|
|
);
|
|
}
|
|
|
|
$schedule = $scheduleId !== null
|
|
? BackupSchedule::query()->withTrashed()->whereKey($scheduleId)->where('managed_environment_id', (int) $run->managed_environment_id)->first()
|
|
: null;
|
|
|
|
if ($schedule instanceof BackupSchedule && $schedule->trashed()) {
|
|
return ReconciliationResult::blocked(
|
|
reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value,
|
|
reasonMessage: 'The backup schedule is archived, so this run never produced a current backup set.',
|
|
evidence: $evidence,
|
|
summaryCounts: [
|
|
'total' => 0,
|
|
'processed' => 0,
|
|
'succeeded' => 0,
|
|
'failed' => 0,
|
|
'skipped' => 1,
|
|
],
|
|
);
|
|
}
|
|
|
|
return ReconciliationResult::notReconciled(
|
|
reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value,
|
|
reasonMessage: 'No current backup set proof is available yet for this backup schedule run.',
|
|
evidence: $evidence,
|
|
);
|
|
}
|
|
|
|
public function reconcileException(OperationRun $run, Throwable $throwable): ?ReconciliationResult
|
|
{
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return array{0:BackupSet|null,1:?string}
|
|
*/
|
|
private function resolveBackupSet(OperationRun $run, ?int $backupSetId): array
|
|
{
|
|
if ($backupSetId === null) {
|
|
return [null, null];
|
|
}
|
|
|
|
$candidate = BackupSet::query()->whereKey($backupSetId)->first();
|
|
|
|
if (! $candidate instanceof BackupSet) {
|
|
return [null, null];
|
|
}
|
|
|
|
if ((int) $candidate->managed_environment_id !== (int) $run->managed_environment_id) {
|
|
return [null, 'The recorded backup set no longer matches the queued backup schedule scope safely.'];
|
|
}
|
|
|
|
return [$candidate, null];
|
|
}
|
|
}
|