fix: collect dataset transfer failures instead of breaking execution

This commit is contained in:
Björn Fromme
2026-04-22 11:23:52 +02:00
parent 9b84893d88
commit d5dfb6c550
2 changed files with 115 additions and 9 deletions
+34 -9
View File
@@ -66,7 +66,7 @@ class BpnXmlSyncCommand extends Command
$totalSyncedCount = 0; $totalSyncedCount = 0;
$totalDeletedCount = 0; $totalDeletedCount = 0;
$hasSyncedFiles = false; $hasSyncedFiles = false;
$hasFailures = false; $failedDatasets = [];
$travelSynced = false; $travelSynced = false;
$travelRemoteTimestamp = null; $travelRemoteTimestamp = null;
$snapshotResult = null; $snapshotResult = null;
@@ -76,7 +76,7 @@ class BpnXmlSyncCommand extends Command
$targetResult = $this->syncTarget($target, $force, $dryRun, $io, $output); $targetResult = $this->syncTarget($target, $force, $dryRun, $io, $output);
if ($targetResult['failed']) { if ($targetResult['failed']) {
$hasFailures = true; $failedDatasets[] = $targetResult['dataset'];
} }
if (false === $targetResult['synced']) { if (false === $targetResult['synced']) {
@@ -109,10 +109,6 @@ class BpnXmlSyncCommand extends Command
} }
} }
if ($hasFailures) {
return Command::FAILURE;
}
if ($dryRun) { if ($dryRun) {
return Command::SUCCESS; return Command::SUCCESS;
} }
@@ -136,14 +132,38 @@ class BpnXmlSyncCommand extends Command
$successMessage .= sprintf(', orphaned deleted: %d', $orphanedDeleted); $successMessage .= sprintf(', orphaned deleted: %d', $orphanedDeleted);
} }
$io->success($successMessage); if ([] !== $failedDatasets) {
$this->logger->info('XML sync completed', [ $successMessage .= sprintf(', failed datasets: %s', implode(', ', $failedDatasets));
}
$logContext = [
'files_updated' => $totalSyncedCount, 'files_updated' => $totalSyncedCount,
'files_deleted' => $totalDeletedCount, 'files_deleted' => $totalDeletedCount,
'snapshots_processed' => $snapshotResult?->processed, 'snapshots_processed' => $snapshotResult?->processed,
'snapshots_failed' => $snapshotResult?->failed, 'snapshots_failed' => $snapshotResult?->failed,
'orphaned_deleted' => $orphanedDeleted, 'orphaned_deleted' => $orphanedDeleted,
'remote_timestamp' => $travelRemoteTimestamp, 'remote_timestamp' => $travelRemoteTimestamp,
'failed_datasets' => $failedDatasets,
];
if ([] !== $failedDatasets) {
$io->warning($successMessage);
$this->logger->warning('XML sync completed with dataset errors', $logContext);
} else {
$io->success($successMessage);
$this->logger->info('XML sync completed', $logContext);
}
return Command::SUCCESS;
}
if ([] !== $failedDatasets) {
$io->warning(sprintf(
'Sync completed with errors for: %s',
implode(', ', $failedDatasets),
));
$this->logger->warning('XML sync completed with dataset errors', [
'failed_datasets' => $failedDatasets,
]); ]);
return Command::SUCCESS; return Command::SUCCESS;
@@ -155,7 +175,7 @@ class BpnXmlSyncCommand extends Command
} }
/** /**
* @return array{synced: bool, failed: bool, updated: int, deleted: int, travelSynced: bool, remoteTimestamp: ?string} * @return array{dataset: string, synced: bool, failed: bool, updated: int, deleted: int, travelSynced: bool, remoteTimestamp: ?string}
*/ */
private function syncTarget( private function syncTarget(
BpnXmlSyncTarget $target, BpnXmlSyncTarget $target,
@@ -175,6 +195,7 @@ class BpnXmlSyncCommand extends Command
)); ));
return [ return [
'dataset' => $datasetName,
'synced' => false, 'synced' => false,
'failed' => false, 'failed' => false,
'updated' => 0, 'updated' => 0,
@@ -201,6 +222,7 @@ class BpnXmlSyncCommand extends Command
]); ]);
return [ return [
'dataset' => $datasetName,
'synced' => false, 'synced' => false,
'failed' => false, 'failed' => false,
'updated' => 0, 'updated' => 0,
@@ -214,6 +236,7 @@ class BpnXmlSyncCommand extends Command
$io->note(sprintf('[%s] Sync required (dry-run mode, no files downloaded)', $datasetName)); $io->note(sprintf('[%s] Sync required (dry-run mode, no files downloaded)', $datasetName));
return [ return [
'dataset' => $datasetName,
'synced' => false, 'synced' => false,
'failed' => false, 'failed' => false,
'updated' => 0, 'updated' => 0,
@@ -244,6 +267,7 @@ class BpnXmlSyncCommand extends Command
]); ]);
return [ return [
'dataset' => $datasetName,
'synced' => false, 'synced' => false,
'failed' => true, 'failed' => true,
'updated' => 0, 'updated' => 0,
@@ -279,6 +303,7 @@ class BpnXmlSyncCommand extends Command
]); ]);
return [ return [
'dataset' => $datasetName,
'synced' => true, 'synced' => true,
'failed' => false, 'failed' => false,
'updated' => $syncResult['updated'], 'updated' => $syncResult['updated'],
+81
View File
@@ -12,6 +12,7 @@ use App\Service\TravelDataProvider;
use App\Service\TravelSnapshotManager; use App\Service\TravelSnapshotManager;
use League\Flysystem\DirectoryListing; use League\Flysystem\DirectoryListing;
use League\Flysystem\FilesystemOperator; use League\Flysystem\FilesystemOperator;
use League\Flysystem\UnableToListContents;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@@ -193,4 +194,84 @@ class BpnXmlSyncCommandTest extends TestCase
$this->assertSame(Command::SUCCESS, $tester->getStatusCode()); $this->assertSame(Command::SUCCESS, $tester->getStatusCode());
} }
public function testContingentTransferFailureDoesNotFailSuccessfulTravelSync(): void
{
$newerTimestamp = "24.03.2026 12:00:00\nExport\n3 Dateien\n";
$olderTimestamp = "24.03.2026 10:00:00\nExport\n3 Dateien\n";
$this->xmlSource->method('read')->willReturn($newerTimestamp);
$this->xmlExport->method('fileExists')->willReturn(true);
$this->xmlExport->method('read')->willReturn($olderTimestamp);
$this->xmlSource->method('listContents')->willReturn(new DirectoryListing([]));
$this->xmlExport->method('listContents')->willReturn(new DirectoryListing([]));
$this->xmlSourceContingents->method('read')->willReturn($newerTimestamp);
$this->xmlExportContingents->method('fileExists')->willReturn(true);
$this->xmlExportContingents->method('read')->willReturn($olderTimestamp);
$this->xmlSourceContingents->method('listContents')
->willThrowException(UnableToListContents::atLocation('.', false, new \RuntimeException('SFTP unavailable')));
$fileMap = [
101 => ['hotels' => ['H1' => null]],
];
$this->travelLoader->expects($this->once())
->method('generateFilesMap')
->willReturn($fileMap);
$this->travelDataService->expects($this->once())
->method('syncSnapshotsFromXml')
->with($fileMap, $this->isInstanceOf(\Closure::class))
->willReturn(['processed' => 1, 'failed' => 0]);
$this->travelSnapshotService->expects($this->once())
->method('purgeOrphanedFutureSnapshots')
->with([101])
->willReturn(0);
$this->cache->expects($this->once())
->method('invalidateTags')
->with(['xml-sync']);
$tester = new CommandTester($this->command);
$tester->execute([]);
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
$this->assertStringContainsString('[contingents] Sync failed', $tester->getDisplay());
$this->assertStringContainsString('datasets: contingents', $tester->getDisplay());
}
public function testTravelTransferFailureDoesNotPreventContingentsSync(): void
{
$newerTimestamp = "24.03.2026 12:00:00\nExport\n3 Dateien\n";
$olderTimestamp = "24.03.2026 10:00:00\nExport\n3 Dateien\n";
$this->xmlSource->method('read')->willReturn($newerTimestamp);
$this->xmlExport->method('fileExists')->willReturn(true);
$this->xmlExport->method('read')->willReturn($olderTimestamp);
$this->xmlSource->method('listContents')
->willThrowException(UnableToListContents::atLocation('.', false, new \RuntimeException('SFTP unavailable')));
$this->xmlSourceContingents->method('read')->willReturn($newerTimestamp);
$this->xmlExportContingents->method('fileExists')->willReturn(true);
$this->xmlExportContingents->method('read')->willReturn($olderTimestamp);
$this->xmlSourceContingents->method('listContents')->willReturn(new DirectoryListing([]));
$this->xmlExportContingents->method('listContents')->willReturn(new DirectoryListing([]));
$this->travelLoader->expects($this->never())->method('generateFilesMap');
$this->travelDataService->expects($this->never())->method('syncSnapshotsFromXml');
$this->travelSnapshotService->expects($this->never())->method('purgeOrphanedFutureSnapshots');
$this->cache->expects($this->once())
->method('invalidateTags')
->with(['xml-sync']);
$tester = new CommandTester($this->command);
$tester->execute([]);
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
$this->assertStringContainsString('[travel] Sync failed', $tester->getDisplay());
$this->assertStringContainsString('[contingents] Synced 0 files, deleted 0', $tester->getDisplay());
$this->assertStringContainsString('datasets: travel', $tester->getDisplay());
}
} }