Fix some cases of database download in GeolocationDbUpdater

This commit is contained in:
Alejandro Celaya
2024-12-15 11:34:38 +01:00
parent f10a9d3972
commit 853c50a819
5 changed files with 46 additions and 33 deletions

View File

@@ -51,6 +51,11 @@ class DownloadGeoLiteDbCommand extends Command implements GeolocationDownloadPro
return ExitCode::EXIT_WARNING; return ExitCode::EXIT_WARNING;
} }
if ($result === GeolocationResult::MAX_ERRORS_REACHED) {
$this->io->warning('Max consecutive errors reached. Cannot retry for a couple of days.');
return ExitCode::EXIT_WARNING;
}
if ($this->progressBar === null) { if ($this->progressBar === null) {
$this->io->info('GeoLite2 db file is up to date.'); $this->io->info('GeoLite2 db file is up to date.');
} else { } else {

View File

@@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand; use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
@@ -74,17 +75,17 @@ class DownloadGeoLiteDbCommandTest extends TestCase
} }
#[Test] #[Test]
public function warningIsPrintedWhenLicenseIsMissing(): void #[TestWith([GeolocationResult::LICENSE_MISSING, 'It was not possible to download GeoLite2 db'])]
#[TestWith([GeolocationResult::MAX_ERRORS_REACHED, 'Max consecutive errors reached'])]
public function warningIsPrintedForSomeResults(GeolocationResult $result, string $expectedWarningMessage): void
{ {
$this->dbUpdater->expects($this->once())->method('checkDbUpdate')->withAnyParameters()->willReturn( $this->dbUpdater->expects($this->once())->method('checkDbUpdate')->withAnyParameters()->willReturn($result);
GeolocationResult::LICENSE_MISSING,
);
$this->commandTester->execute([]); $this->commandTester->execute([]);
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
$exitCode = $this->commandTester->getStatusCode(); $exitCode = $this->commandTester->getStatusCode();
self::assertStringContainsString('[WARNING] It was not possible to download GeoLite2 db', $output); self::assertStringContainsString('[WARNING] ' . $expectedWarningMessage, $output);
self::assertSame(ExitCode::EXIT_WARNING, $exitCode); self::assertSame(ExitCode::EXIT_WARNING, $exitCode);
} }

View File

@@ -49,17 +49,11 @@ class GeolocationDbUpdate extends AbstractEntity
} }
/** /**
* This update would require a new download if: * @param positive-int $days
* - It is successful and older than 30 days
* - It is error and older than 2 days
*/ */
public function needsUpdate(): bool public function isOlderThan(int $days): bool
{ {
return match ($this->status) { return Chronos::now()->greaterThan($this->dateUpdated->addDays($days));
GeolocationDbUpdateStatus::SUCCESS => Chronos::now()->greaterThan($this->dateUpdated->addDays(30)),
GeolocationDbUpdateStatus::ERROR => Chronos::now()->greaterThan($this->dateUpdated->addDays(2)),
default => false,
};
} }
public function isInProgress(): bool public function isInProgress(): bool
@@ -71,4 +65,9 @@ class GeolocationDbUpdate extends AbstractEntity
{ {
return $this->status === GeolocationDbUpdateStatus::ERROR; return $this->status === GeolocationDbUpdateStatus::ERROR;
} }
public function isSuccess(): bool
{
return $this->status === GeolocationDbUpdateStatus::SUCCESS;
}
} }

View File

@@ -13,8 +13,6 @@ use Shlinkio\Shlink\IpGeolocation\Exception\MissingLicenseException;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockFactory;
use function count;
use function Shlinkio\Shlink\Core\ArrayUtils\every;
use function sprintf; use function sprintf;
readonly class GeolocationDbUpdater implements GeolocationDbUpdaterInterface readonly class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
@@ -26,6 +24,7 @@ readonly class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
private LockFactory $locker, private LockFactory $locker,
private TrackingOptions $trackingOptions, private TrackingOptions $trackingOptions,
private EntityManagerInterface $em, private EntityManagerInterface $em,
private int $maxRecentAttemptsToCheck = 15, // TODO Make this configurable
) { ) {
} }
@@ -56,16 +55,12 @@ readonly class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
private function downloadIfNeeded( private function downloadIfNeeded(
GeolocationDownloadProgressHandlerInterface|null $downloadProgressHandler, GeolocationDownloadProgressHandlerInterface|null $downloadProgressHandler,
): GeolocationResult { ): GeolocationResult {
$maxRecentAttemptsToCheck = 15; // TODO Make this configurable
// Get last 15 download attempts
$recentDownloads = $this->em->getRepository(GeolocationDbUpdate::class)->findBy( $recentDownloads = $this->em->getRepository(GeolocationDbUpdate::class)->findBy(
criteria: ['filesystemId' => GeolocationDbUpdate::currentFilesystemId()], criteria: ['filesystemId' => GeolocationDbUpdate::currentFilesystemId()],
orderBy: ['dateUpdated' => 'DESC'], orderBy: ['dateUpdated' => 'DESC'],
limit: $maxRecentAttemptsToCheck, limit: $this->maxRecentAttemptsToCheck,
); );
$mostRecentDownload = $recentDownloads[0] ?? null; $mostRecentDownload = $recentDownloads[0] ?? null;
$amountOfRecentAttempts = count($recentDownloads);
// If most recent attempt is in progress, skip check. // If most recent attempt is in progress, skip check.
// This is a safety check in case the lock is released before the previous download has finished. // This is a safety check in case the lock is released before the previous download has finished.
@@ -73,19 +68,31 @@ readonly class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
return GeolocationResult::CHECK_SKIPPED; return GeolocationResult::CHECK_SKIPPED;
} }
// If all recent attempts are errors, and the most recent one is not old enough, skip download $amountOfErrorsSinceLastSuccess = 0;
if ( foreach ($recentDownloads as $recentDownload) {
$amountOfRecentAttempts === $maxRecentAttemptsToCheck // Count attempts until a success is found
&& every($recentDownloads, static fn (GeolocationDbUpdate $update) => $update->isError()) if ($recentDownload->isSuccess()) {
&& ! $mostRecentDownload->needsUpdate() break;
) { }
return GeolocationResult::CHECK_SKIPPED; $amountOfErrorsSinceLastSuccess++;
} }
// Try to download if there are no attempts, the database file does not exist or most recent attempt was // If max amount of consecutive errors has been reached and the most recent one is not old enough, skip download
// successful and is old enough // for 2 days to avoid hitting potential API limits in geolocation services
$olderDbExists = $amountOfRecentAttempts > 0 && $this->dbUpdater->databaseFileExists(); $lastAttemptIsError = $mostRecentDownload !== null && $mostRecentDownload->isError();
if (! $olderDbExists || $mostRecentDownload->needsUpdate()) { // FIXME Once max errors are reached there will be one attempt every 2 days, but it should be 15 attempts every
// 2 days. Leaving like this for simplicity for now.
$maxConsecutiveErrorsReached = $amountOfErrorsSinceLastSuccess === $this->maxRecentAttemptsToCheck;
if ($lastAttemptIsError && $maxConsecutiveErrorsReached && ! $mostRecentDownload->isOlderThan(days: 2)) {
return GeolocationResult::MAX_ERRORS_REACHED;
}
// Try to download if:
// - There are no attempts or the database file does not exist
// - Last update errored (and implicitly, the max amount of consecutive errors has not been reached)
// - Most recent attempt is older than 30 days (and implicitly, successful)
$olderDbExists = $mostRecentDownload !== null && $this->dbUpdater->databaseFileExists();
if (! $olderDbExists || $lastAttemptIsError || $mostRecentDownload->isOlderThan(days: 30)) {
return $this->downloadAndTrackUpdate($downloadProgressHandler, $olderDbExists); return $this->downloadAndTrackUpdate($downloadProgressHandler, $olderDbExists);
} }
@@ -112,7 +119,7 @@ readonly class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
return GeolocationResult::LICENSE_MISSING; return GeolocationResult::LICENSE_MISSING;
} catch (GeolocationDbUpdateFailedException $e) { } catch (GeolocationDbUpdateFailedException $e) {
$dbUpdate->finishWithError( $dbUpdate->finishWithError(
sprintf('%s. Prev: %s', $e->getMessage(), $e->getPrevious()?->getMessage() ?? '-'), sprintf('%s Prev: %s', $e->getMessage(), $e->getPrevious()?->getMessage() ?? '-'),
); );
throw $e; throw $e;
} finally { } finally {

View File

@@ -5,6 +5,7 @@ namespace Shlinkio\Shlink\Core\Geolocation;
enum GeolocationResult enum GeolocationResult
{ {
case CHECK_SKIPPED; case CHECK_SKIPPED;
case MAX_ERRORS_REACHED;
case LICENSE_MISSING; case LICENSE_MISSING;
case DB_CREATED; case DB_CREATED;
case DB_UPDATED; case DB_UPDATED;