Allowed to delay GeoLite2 db download on docker images

This commit is contained in:
Alejandro Celaya 2022-09-18 17:00:03 +02:00
parent ef01754ad5
commit 6f17f70137
8 changed files with 95 additions and 23 deletions

View File

@ -50,7 +50,7 @@
"shlinkio/shlink-event-dispatcher": "dev-main#48c0137 as 2.6", "shlinkio/shlink-event-dispatcher": "dev-main#48c0137 as 2.6",
"shlinkio/shlink-importer": "^4.0", "shlinkio/shlink-importer": "^4.0",
"shlinkio/shlink-installer": "dev-develop#a01bca9 as 8.2", "shlinkio/shlink-installer": "dev-develop#a01bca9 as 8.2",
"shlinkio/shlink-ip-geolocation": "^3.0", "shlinkio/shlink-ip-geolocation": "^3.1",
"spiral/roadrunner": "^2.11", "spiral/roadrunner": "^2.11",
"spiral/roadrunner-jobs": "^2.3", "spiral/roadrunner-jobs": "^2.3",
"symfony/console": "^6.1", "symfony/console": "^6.1",

View File

@ -18,14 +18,14 @@ php bin/doctrine orm:generate-proxies -n ${flags}
echo "Clearing entities cache..." echo "Clearing entities cache..."
php bin/doctrine orm:clear-cache:metadata -n ${flags} php bin/doctrine orm:clear-cache:metadata -n ${flags}
# Try to download GeoLite2 db file only if the license key env var was defined # Try to download GeoLite2 db file only if the license key env var was defined and skipping was not explicitly set
if [ ! -z "${GEOLITE_LICENSE_KEY}" ]; then if [ ! -z "${GEOLITE_LICENSE_KEY}" ] && [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" != "true" ]; then
echo "Downloading GeoLite2 db file..." echo "Downloading GeoLite2 db file..."
php bin/cli visit:download-db -n ${flags} php bin/cli visit:download-db -n ${flags}
fi fi
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided # Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided
if [ $ENABLE_PERIODIC_VISIT_LOCATE ]; then if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ]; then
echo "Configuring periodic visit location..." echo "Configuring periodic visit location..."
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
/usr/sbin/crond & /usr/sbin/crond &

View File

@ -19,6 +19,7 @@ use Shlinkio\Shlink\Core\Tag\TagService;
use Shlinkio\Shlink\Core\Visit; use Shlinkio\Shlink\Core\Visit;
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory; use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2Options;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Symfony\Component\Console as SymfonyCli; use Symfony\Component\Console as SymfonyCli;
@ -75,6 +76,7 @@ return [
Reader::class, Reader::class,
LOCAL_LOCK_FACTORY, LOCAL_LOCK_FACTORY,
TrackingOptions::class, TrackingOptions::class,
GeoLite2Options::class,
], ],
Util\ProcessRunner::class => [SymfonyCli\Helper\ProcessHelper::class], Util\ProcessRunner::class => [SymfonyCli\Helper\ProcessHelper::class],
ApiKey\RoleResolver::class => [DomainService::class, 'config.url_shortener.domain.hostname'], ApiKey\RoleResolver::class => [DomainService::class, 'config.url_shortener.domain.hostname'],

View File

@ -34,8 +34,8 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
private SymfonyStyle $io; private SymfonyStyle $io;
public function __construct( public function __construct(
private VisitLocatorInterface $visitLocator, private readonly VisitLocatorInterface $visitLocator,
private IpLocationResolverInterface $ipLocationResolver, private readonly IpLocationResolverInterface $ipLocationResolver,
LockFactory $locker, LockFactory $locker,
) { ) {
parent::__construct($locker); parent::__construct($locker);

View File

@ -9,7 +9,9 @@ use GeoIp2\Database\Reader;
use MaxMind\Db\Reader\Metadata; use MaxMind\Db\Reader\Metadata;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\Core\Options\TrackingOptions; use Shlinkio\Shlink\Core\Options\TrackingOptions;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException; use Shlinkio\Shlink\IpGeolocation\Exception\DbUpdateException;
use Shlinkio\Shlink\IpGeolocation\Exception\MissingLicenseException;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockFactory;
@ -20,10 +22,10 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
private const LOCK_NAME = 'geolocation-db-update'; private const LOCK_NAME = 'geolocation-db-update';
public function __construct( public function __construct(
private DbUpdaterInterface $dbUpdater, private readonly DbUpdaterInterface $dbUpdater,
private Reader $geoLiteDbReader, private readonly Reader $geoLiteDbReader,
private LockFactory $locker, private readonly LockFactory $locker,
private TrackingOptions $trackingOptions, private readonly TrackingOptions $trackingOptions,
) { ) {
} }
@ -52,14 +54,12 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
private function downloadIfNeeded(?callable $beforeDownload, ?callable $handleProgress): GeolocationResult private function downloadIfNeeded(?callable $beforeDownload, ?callable $handleProgress): GeolocationResult
{ {
if (! $this->dbUpdater->databaseFileExists()) { if (! $this->dbUpdater->databaseFileExists()) {
$this->downloadNewDb(false, $beforeDownload, $handleProgress); return $this->downloadNewDb(false, $beforeDownload, $handleProgress);
return GeolocationResult::DB_CREATED;
} }
$meta = $this->geoLiteDbReader->metadata(); $meta = $this->geoLiteDbReader->metadata();
if ($this->buildIsTooOld($meta)) { if ($this->buildIsTooOld($meta)) {
$this->downloadNewDb(true, $beforeDownload, $handleProgress); return $this->downloadNewDb(true, $beforeDownload, $handleProgress);
return GeolocationResult::DB_UPDATED;
} }
return GeolocationResult::DB_IS_UP_TO_DATE; return GeolocationResult::DB_IS_UP_TO_DATE;
@ -95,15 +95,22 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
/** /**
* @throws GeolocationDbUpdateFailedException * @throws GeolocationDbUpdateFailedException
*/ */
private function downloadNewDb(bool $olderDbExists, ?callable $beforeDownload, ?callable $handleProgress): void private function downloadNewDb(
{ bool $olderDbExists,
?callable $beforeDownload,
?callable $handleProgress,
): GeolocationResult {
if ($beforeDownload !== null) { if ($beforeDownload !== null) {
$beforeDownload($olderDbExists); $beforeDownload($olderDbExists);
} }
try { try {
$this->dbUpdater->downloadFreshCopy($this->wrapHandleProgressCallback($handleProgress, $olderDbExists)); $this->dbUpdater->downloadFreshCopy($this->wrapHandleProgressCallback($handleProgress, $olderDbExists));
} catch (RuntimeException $e) { return $olderDbExists ? GeolocationResult::DB_UPDATED : GeolocationResult::DB_CREATED;
} catch (MissingLicenseException) {
// If there's no license key, just ignore the error
return GeolocationResult::CHECK_SKIPPED;
} catch (DbUpdateException | WrongIpException $e) {
throw $olderDbExists throw $olderDbExists
? GeolocationDbUpdateFailedException::withOlderDb($e) ? GeolocationDbUpdateFailedException::withOlderDb($e)
: GeolocationDbUpdateFailedException::withoutOlderDb($e); : GeolocationDbUpdateFailedException::withoutOlderDb($e);
@ -116,6 +123,6 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
return null; return null;
} }
return fn (int $total, int $downloaded) => $handleProgress($total, $downloaded, $olderDbExists); return static fn (int $total, int $downloaded) => $handleProgress($total, $downloaded, $olderDbExists);
} }
} }

View File

@ -15,7 +15,7 @@ use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater; use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater;
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult; use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
use Shlinkio\Shlink\Core\Options\TrackingOptions; use Shlinkio\Shlink\Core\Options\TrackingOptions;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException; use Shlinkio\Shlink\IpGeolocation\Exception\DbUpdateException;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Lock; use Symfony\Component\Lock;
use Throwable; use Throwable;
@ -48,7 +48,7 @@ class GeolocationDbUpdaterTest extends TestCase
public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void
{ {
$mustBeUpdated = fn () => self::assertTrue(true); $mustBeUpdated = fn () => self::assertTrue(true);
$prev = new RuntimeException(''); $prev = new DbUpdateException('');
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(false); $fileExists = $this->dbUpdater->databaseFileExists()->willReturn(false);
$getMeta = $this->geoLiteDbReader->metadata(); $getMeta = $this->geoLiteDbReader->metadata();
@ -81,7 +81,7 @@ class GeolocationDbUpdaterTest extends TestCase
$getMeta = $this->geoLiteDbReader->metadata()->willReturn($this->buildMetaWithBuildEpoch( $getMeta = $this->geoLiteDbReader->metadata()->willReturn($this->buildMetaWithBuildEpoch(
Chronos::now()->subDays($days)->getTimestamp(), Chronos::now()->subDays($days)->getTimestamp(),
)); ));
$prev = new RuntimeException(''); $prev = new DbUpdateException('');
$download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev); $download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev);
try { try {

View File

@ -10,6 +10,7 @@ use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater;
use Shlinkio\Shlink\Common\Cache\RedisPublishingHelper; use Shlinkio\Shlink\Common\Cache\RedisPublishingHelper;
use Shlinkio\Shlink\Common\Mercure\MercureHubPublishingHelper; use Shlinkio\Shlink\Common\Mercure\MercureHubPublishingHelper;
use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelper; use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelper;
use Shlinkio\Shlink\Core\Visit\VisitLocator;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
@ -21,7 +22,7 @@ return [
EventDispatcher\LocateVisit::class, EventDispatcher\LocateVisit::class,
], ],
EventDispatcher\Event\GeoLiteDbCreated::class => [ EventDispatcher\Event\GeoLiteDbCreated::class => [
// EventDispatcher\LocateUnloctedVisits::class, EventDispatcher\LocateUnlocatedVisits::class,
], ],
], ],
'async' => [ 'async' => [
@ -43,6 +44,7 @@ return [
'dependencies' => [ 'dependencies' => [
'factories' => [ 'factories' => [
EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class, EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class,
EventDispatcher\LocateUnlocatedVisits::class => ConfigAbstractFactory::class,
EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class, EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class,
EventDispatcher\Mercure\NotifyVisitToMercure::class => ConfigAbstractFactory::class, EventDispatcher\Mercure\NotifyVisitToMercure::class => ConfigAbstractFactory::class,
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => ConfigAbstractFactory::class, EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => ConfigAbstractFactory::class,
@ -72,6 +74,9 @@ return [
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [ EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [
EventDispatcher\CloseDbConnectionEventListenerDelegator::class, EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
], ],
EventDispatcher\LocateUnlocatedVisits::class => [
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
],
EventDispatcher\NotifyVisitToWebHooks::class => [ EventDispatcher\NotifyVisitToWebHooks::class => [
EventDispatcher\CloseDbConnectionEventListenerDelegator::class, EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
], ],
@ -86,6 +91,7 @@ return [
DbUpdater::class, DbUpdater::class,
EventDispatcherInterface::class, EventDispatcherInterface::class,
], ],
EventDispatcher\LocateUnlocatedVisits::class => [VisitLocator::class, IpLocationResolverInterface::class],
EventDispatcher\NotifyVisitToWebHooks::class => [ EventDispatcher\NotifyVisitToWebHooks::class => [
'httpClient', 'httpClient',
'em', 'em',

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\EventDispatcher;
use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\EventDispatcher\Event\GeoLiteDbCreated;
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface;
use Shlinkio\Shlink\Core\Visit\VisitLocatorInterface;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
class LocateUnlocatedVisits implements VisitGeolocationHelperInterface
{
public function __construct(
private readonly VisitLocatorInterface $locator,
private readonly IpLocationResolverInterface $ipLocationResolver,
) {
}
public function __invoke(GeoLiteDbCreated $event): void
{
$this->locator->locateUnlocatedVisits($this);
}
/**
* @throws IpCannotBeLocatedException
*/
public function geolocateVisit(Visit $visit): Location
{
// TODO This method duplicates code from LocateVisitsCommand. Move to a common place.
if (! $visit->hasRemoteAddr()) {
throw IpCannotBeLocatedException::forEmptyAddress();
}
$ipAddr = $visit->getRemoteAddr() ?? '';
if ($ipAddr === IpAddress::LOCALHOST) {
throw IpCannotBeLocatedException::forLocalhost();
}
try {
return $this->ipLocationResolver->resolveIpLocation($ipAddr);
} catch (WrongIpException $e) {
throw IpCannotBeLocatedException::forError($e);
}
}
public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void
{
// Do nothing
}
}