TenantAtlas/app/Console/Commands/OpsReconcileAdapterRuns.php
2026-01-19 18:50:11 +01:00

117 lines
4.2 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Services\AdapterRunReconciler;
use Illuminate\Console\Command;
use Throwable;
class OpsReconcileAdapterRuns extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ops:reconcile-adapter-runs
{--type= : Adapter run type (e.g. restore.execute)}
{--tenant= : Tenant ID}
{--older-than=60 : Only consider runs older than N minutes}
{--dry-run=true : Preview only (true/false)}
{--limit=50 : Max number of runs to inspect}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Reconcile stale adapter-backed operation runs from DB-only source-of-truth records.';
/**
* Execute the console command.
*/
public function handle()
{
try {
/** @var AdapterRunReconciler $reconciler */
$reconciler = app(AdapterRunReconciler::class);
$type = $this->option('type');
$type = is_string($type) && trim($type) !== '' ? trim($type) : null;
$tenantId = $this->option('tenant');
$tenantId = is_numeric($tenantId) ? (int) $tenantId : null;
$olderThanMinutes = $this->option('older-than');
$olderThanMinutes = is_numeric($olderThanMinutes) ? (int) $olderThanMinutes : 60;
$olderThanMinutes = max(1, $olderThanMinutes);
$limit = $this->option('limit');
$limit = is_numeric($limit) ? (int) $limit : 50;
$limit = max(1, $limit);
$dryRun = $this->option('dry-run');
$dryRun = filter_var($dryRun, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE);
$dryRun = $dryRun ?? true;
$result = $reconciler->reconcile([
'type' => $type,
'tenant_id' => $tenantId,
'older_than_minutes' => $olderThanMinutes,
'limit' => $limit,
'dry_run' => $dryRun,
]);
$changes = $result['changes'] ?? [];
usort($changes, static fn (array $a, array $b): int => ((int) ($a['operation_run_id'] ?? 0)) <=> ((int) ($b['operation_run_id'] ?? 0)));
$this->info('Adapter run reconciliation');
$this->line('dry_run: '.($dryRun ? 'true' : 'false'));
$this->line('type: '.($type ?? '(all supported)'));
$this->line('tenant: '.($tenantId ? (string) $tenantId : '(all)'));
$this->line('older_than_minutes: '.$olderThanMinutes);
$this->line('limit: '.$limit);
$this->newLine();
$this->line('candidates: '.(int) ($result['candidates'] ?? 0));
$this->line('reconciled: '.(int) ($result['reconciled'] ?? 0));
$this->line('skipped: '.(int) ($result['skipped'] ?? 0));
$this->newLine();
if ($changes === []) {
$this->info('No changes.');
return self::SUCCESS;
}
$rows = [];
foreach ($changes as $change) {
$before = is_array($change['before'] ?? null) ? $change['before'] : [];
$after = is_array($change['after'] ?? null) ? $change['after'] : [];
$rows[] = [
'applied' => ($change['applied'] ?? false) ? 'yes' : 'no',
'operation_run_id' => (int) ($change['operation_run_id'] ?? 0),
'type' => (string) ($change['type'] ?? ''),
'source_id' => (int) ($change['restore_run_id'] ?? 0),
'before' => (string) (($before['status'] ?? '').'/'.($before['outcome'] ?? '')),
'after' => (string) (($after['status'] ?? '').'/'.($after['outcome'] ?? '')),
];
}
$this->table(
['applied', 'operation_run_id', 'type', 'source_id', 'before', 'after'],
$rows,
);
return self::SUCCESS;
} catch (Throwable $e) {
$this->error('Reconciliation failed: '.$e->getMessage());
return self::FAILURE;
}
}
}