237 lines
7.3 KiB
PHP
237 lines
7.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\OperationRun;
|
|
use App\Support\OperationCatalog;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\Operations\Reconciliation\OperationRunReconciliationAdapter;
|
|
use App\Support\Operations\Reconciliation\OperationRunReconciliationRegistry;
|
|
use App\Support\Operations\Reconciliation\ReconciliationResult;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Throwable;
|
|
|
|
final class AdapterRunReconciler
|
|
{
|
|
public function __construct(
|
|
private readonly OperationRunReconciliationRegistry $registry,
|
|
private readonly OperationRunService $operationRuns,
|
|
) {}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
public function supportedTypes(): array
|
|
{
|
|
return $this->registry->supportedTypes();
|
|
}
|
|
|
|
/**
|
|
* @param array{type?: string|null, managed_environment_id?: int|null, older_than_minutes?: int, limit?: int, dry_run?: bool} $options
|
|
* @return array{candidates:int,reconciled:int,skipped:int,changes:array<int,array<string,mixed>>}
|
|
*/
|
|
public function reconcile(array $options = []): array
|
|
{
|
|
$type = $this->normalizeRequestedType($options['type'] ?? null);
|
|
$tenantId = $options['managed_environment_id'] ?? null;
|
|
$olderThanMinutes = max(1, (int) ($options['older_than_minutes'] ?? 10));
|
|
$limit = max(1, (int) ($options['limit'] ?? 50));
|
|
$dryRun = (bool) ($options['dry_run'] ?? true);
|
|
|
|
$cutoff = CarbonImmutable::now()->subMinutes($olderThanMinutes);
|
|
$candidateTypes = $this->candidateRawTypes($type);
|
|
|
|
$query = OperationRun::query()
|
|
->whereIn('type', $candidateTypes)
|
|
->whereIn('status', [OperationRunStatus::Queued->value, OperationRunStatus::Running->value])
|
|
->where(function (Builder $q) use ($cutoff): void {
|
|
$q
|
|
->where(function (Builder $q2) use ($cutoff): void {
|
|
$q2->whereNull('started_at')->where('created_at', '<', $cutoff);
|
|
})
|
|
->orWhere(function (Builder $q2) use ($cutoff): void {
|
|
$q2->whereNotNull('started_at')->where('started_at', '<', $cutoff);
|
|
});
|
|
})
|
|
->orderBy('id')
|
|
->limit($limit);
|
|
|
|
if (is_int($tenantId) && $tenantId > 0) {
|
|
$query->where('managed_environment_id', $tenantId);
|
|
}
|
|
|
|
$candidates = $query->get();
|
|
|
|
$changes = [];
|
|
$reconciled = 0;
|
|
$skipped = 0;
|
|
|
|
foreach ($candidates as $run) {
|
|
$change = $this->reconcileOperationRun($run, $dryRun);
|
|
|
|
if ($change === null) {
|
|
$skipped++;
|
|
|
|
continue;
|
|
}
|
|
|
|
$changes[] = $change;
|
|
|
|
if (($change['applied'] ?? false) === true) {
|
|
$reconciled++;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'candidates' => $candidates->count(),
|
|
'reconciled' => $reconciled,
|
|
'skipped' => $skipped,
|
|
'changes' => $changes,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public function reconcileOperationRun(OperationRun $run, bool $dryRun = false): ?array
|
|
{
|
|
$adapter = $this->resolveAdapter($run);
|
|
|
|
return $this->reconcileUsingAdapter(
|
|
run: $run,
|
|
result: $adapter?->reconcile($run),
|
|
adapter: $adapter,
|
|
dryRun: $dryRun,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public function reconcileOperationRunFailure(OperationRun $run, Throwable $throwable, bool $dryRun = false): ?array
|
|
{
|
|
$adapter = $this->resolveAdapter($run);
|
|
|
|
return $this->reconcileUsingAdapter(
|
|
run: $run,
|
|
result: $adapter?->reconcileException($run, $throwable),
|
|
adapter: $adapter,
|
|
dryRun: $dryRun,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private function reconcileUsingAdapter(
|
|
OperationRun $run,
|
|
?ReconciliationResult $result,
|
|
?OperationRunReconciliationAdapter $adapter,
|
|
bool $dryRun,
|
|
): ?array {
|
|
if (! $adapter instanceof OperationRunReconciliationAdapter || ! $result instanceof ReconciliationResult) {
|
|
return null;
|
|
}
|
|
|
|
$before = [
|
|
'status' => (string) $run->status,
|
|
'outcome' => (string) $run->outcome,
|
|
];
|
|
|
|
$after = $result->shouldFinalizeRun()
|
|
? [
|
|
'status' => (string) $result->status,
|
|
'outcome' => (string) $result->outcome,
|
|
]
|
|
: null;
|
|
|
|
if ($dryRun) {
|
|
return array_merge([
|
|
'applied' => false,
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'type' => (string) $run->type,
|
|
'adapter' => $adapter->key(),
|
|
'before' => $before,
|
|
], $result->toArray(), $after !== null ? ['after' => $after] : []);
|
|
}
|
|
|
|
if (! $result->shouldFinalizeRun()) {
|
|
return null;
|
|
}
|
|
|
|
$this->operationRuns->applyReconciliationResult(
|
|
run: $run,
|
|
result: $result,
|
|
source: 'adapter_reconciler',
|
|
adapter: $adapter->key(),
|
|
);
|
|
|
|
$run->refresh();
|
|
$this->syncRestoreLifecycleTimestamps($run, $result);
|
|
|
|
return array_merge([
|
|
'applied' => true,
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'type' => (string) $run->type,
|
|
'adapter' => $adapter->key(),
|
|
'before' => $before,
|
|
'after' => $after ?? [],
|
|
], $result->toArray());
|
|
}
|
|
|
|
private function resolveAdapter(OperationRun $run): ?OperationRunReconciliationAdapter
|
|
{
|
|
return $this->registry->forType($run->canonicalOperationType());
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function candidateRawTypes(?string $type = null): array
|
|
{
|
|
$canonicalTypes = $type !== null ? [$type] : $this->supportedTypes();
|
|
|
|
return array_values(array_unique(array_merge(
|
|
...array_map(
|
|
static fn (string $canonicalType): array => OperationCatalog::rawValuesForCanonical($canonicalType),
|
|
$canonicalTypes,
|
|
),
|
|
)));
|
|
}
|
|
|
|
private function normalizeRequestedType(mixed $type): ?string
|
|
{
|
|
if (! is_string($type) || trim($type) === '') {
|
|
return null;
|
|
}
|
|
|
|
$canonicalType = OperationCatalog::canonicalCode($type);
|
|
|
|
if (! in_array($canonicalType, $this->supportedTypes(), true)) {
|
|
throw new \InvalidArgumentException('Unsupported adapter run type: '.$type);
|
|
}
|
|
|
|
return $canonicalType;
|
|
}
|
|
|
|
private function syncRestoreLifecycleTimestamps(OperationRun $run, ReconciliationResult $result): void
|
|
{
|
|
if (! array_key_exists('restore_run_id', $result->evidence)) {
|
|
return;
|
|
}
|
|
|
|
if ($run->started_at === null && is_string($result->evidence['restore_started_at'] ?? null)) {
|
|
$run->started_at = $result->evidence['restore_started_at'];
|
|
}
|
|
|
|
if ($run->completed_at === null && is_string($result->evidence['restore_completed_at'] ?? null)) {
|
|
$run->completed_at = $result->evidence['restore_completed_at'];
|
|
}
|
|
|
|
$run->save();
|
|
}
|
|
}
|